Spaces:
Running
Running
Merge pull request #34 from nagomi-sugarheart/claude/add-comment-count-time-afn6i
Browse files- CLAUDE.md +1 -1
- main.py +37 -8
- templates/blog_detail.html +96 -18
CLAUDE.md
CHANGED
|
@@ -164,7 +164,7 @@
|
|
| 164 |
| **contentRich** | 本文(リッチ) | リッチテキストエリア | microCMS 管理画面直接編集用 |
|
| 165 |
| **contentBlocks** | ブロックデータ | テキストエリア | ブロック配列 JSON(編集用) |
|
| 166 |
|
| 167 |
-
### 7.2
|
| 168 |
| フィールドID | 表示名 | データ型 | 用途 |
|
| 169 |
| :--- | :--- | :--- | :--- |
|
| 170 |
| **articleId** | 記事ID | テキストフィールド | 対象記事の ID |
|
|
|
|
| 164 |
| **contentRich** | 本文(リッチ) | リッチテキストエリア | microCMS 管理画面直接編集用 |
|
| 165 |
| **contentBlocks** | ブロックデータ | テキストエリア | ブロック配列 JSON(編集用) |
|
| 166 |
|
| 167 |
+
### 7.2 comments(リスト型・コメント)
|
| 168 |
| フィールドID | 表示名 | データ型 | 用途 |
|
| 169 |
| :--- | :--- | :--- | :--- |
|
| 170 |
| **articleId** | 記事ID | テキストフィールド | 対象記事の ID |
|
main.py
CHANGED
|
@@ -166,14 +166,28 @@ def get_comments(article_id: str) -> list:
|
|
| 166 |
except Exception:
|
| 167 |
return []
|
| 168 |
|
| 169 |
-
def post_comment_api(article_id: str, name: str, body: str) -> bool:
|
| 170 |
try:
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
| 173 |
return r.status_code in [200, 201]
|
| 174 |
except Exception:
|
| 175 |
return False
|
| 176 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
def migrate_comments(old_id: str, new_id: str) -> int:
|
| 178 |
try:
|
| 179 |
r = http_requests.get(cms_url("comments"), headers=CMS_HEADERS_GET(),
|
|
@@ -304,17 +318,17 @@ async def index(request: Request, id: str = None, page: int = 0,
|
|
| 304 |
if not article:
|
| 305 |
return templates.TemplateResponse(request, "blog_detail.html",
|
| 306 |
{"article": None, "comments": []})
|
| 307 |
-
|
|
|
|
| 308 |
base_url = SPACE_URL or str(request.base_url).rstrip("/")
|
| 309 |
-
# contentBlocks が存在する場合はブロックから直接レンダリングし
|
| 310 |
-
# microCMS の contentRich フィールドによるスタイル除去を回避する
|
| 311 |
if article.get("contentBlocks"):
|
| 312 |
rendered_content = render_blocks_to_html(blocks_from_article(article))
|
| 313 |
else:
|
| 314 |
rendered_content = clean_html(get_content(article))
|
| 315 |
return templates.TemplateResponse(request, "blog_detail.html", {
|
| 316 |
"article": article,
|
| 317 |
-
"comments":
|
|
|
|
| 318 |
"content": rendered_content,
|
| 319 |
"tags": get_tags(article),
|
| 320 |
"space_url": base_url,
|
|
@@ -462,6 +476,9 @@ async def api_save(request: Request):
|
|
| 462 |
was_draft = existing.get("title", "").startswith("【下書き】")
|
| 463 |
|
| 464 |
if was_draft and becomes_public:
|
|
|
|
|
|
|
|
|
|
| 465 |
http_requests.delete(f"{base_url}/{article_id}", headers=CMS_HEADERS_GET())
|
| 466 |
r = http_requests.post(base_url, headers=CMS_HEADERS_POST(), json=post_data)
|
| 467 |
if r.status_code in [200, 201]:
|
|
@@ -638,7 +655,19 @@ async def api_comment(request: Request):
|
|
| 638 |
article_id = data.get("article_id", "")
|
| 639 |
name = data.get("name", "わせます太郎").strip() or "わせます太郎"
|
| 640 |
body = data.get("body", "").strip()
|
|
|
|
| 641 |
if not body:
|
| 642 |
return JSONResponse({"success": False, "error": "コメントを入力してください"})
|
| 643 |
-
ok = post_comment_api(article_id, name, body)
|
| 644 |
return JSONResponse({"success": ok})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
except Exception:
|
| 167 |
return []
|
| 168 |
|
| 169 |
+
def post_comment_api(article_id: str, name: str, body: str, parent_id: str = "") -> bool:
|
| 170 |
try:
|
| 171 |
+
payload = {"articleId": article_id, "name": name, "body": body}
|
| 172 |
+
if parent_id:
|
| 173 |
+
payload["parentId"] = parent_id
|
| 174 |
+
r = http_requests.post(cms_url("comments"), headers=CMS_HEADERS_POST(), json=payload)
|
| 175 |
return r.status_code in [200, 201]
|
| 176 |
except Exception:
|
| 177 |
return False
|
| 178 |
|
| 179 |
+
def build_comment_tree(comments: list) -> list:
|
| 180 |
+
by_id = {c["id"]: {**c, "replies": []} for c in comments}
|
| 181 |
+
roots = []
|
| 182 |
+
for c in comments:
|
| 183 |
+
node = by_id[c["id"]]
|
| 184 |
+
pid = c.get("parentId", "")
|
| 185 |
+
if pid and pid in by_id:
|
| 186 |
+
by_id[pid]["replies"].append(node)
|
| 187 |
+
else:
|
| 188 |
+
roots.append(node)
|
| 189 |
+
return roots
|
| 190 |
+
|
| 191 |
def migrate_comments(old_id: str, new_id: str) -> int:
|
| 192 |
try:
|
| 193 |
r = http_requests.get(cms_url("comments"), headers=CMS_HEADERS_GET(),
|
|
|
|
| 318 |
if not article:
|
| 319 |
return templates.TemplateResponse(request, "blog_detail.html",
|
| 320 |
{"article": None, "comments": []})
|
| 321 |
+
flat_comments = get_comments(id)
|
| 322 |
+
comment_tree = build_comment_tree(flat_comments)
|
| 323 |
base_url = SPACE_URL or str(request.base_url).rstrip("/")
|
|
|
|
|
|
|
| 324 |
if article.get("contentBlocks"):
|
| 325 |
rendered_content = render_blocks_to_html(blocks_from_article(article))
|
| 326 |
else:
|
| 327 |
rendered_content = clean_html(get_content(article))
|
| 328 |
return templates.TemplateResponse(request, "blog_detail.html", {
|
| 329 |
"article": article,
|
| 330 |
+
"comments": comment_tree,
|
| 331 |
+
"comment_count": len(flat_comments),
|
| 332 |
"content": rendered_content,
|
| 333 |
"tags": get_tags(article),
|
| 334 |
"space_url": base_url,
|
|
|
|
| 476 |
was_draft = existing.get("title", "").startswith("【下書き】")
|
| 477 |
|
| 478 |
if was_draft and becomes_public:
|
| 479 |
+
likes_count = int(existing.get("likes", 0) or 0)
|
| 480 |
+
if likes_count:
|
| 481 |
+
post_data["likes"] = likes_count
|
| 482 |
http_requests.delete(f"{base_url}/{article_id}", headers=CMS_HEADERS_GET())
|
| 483 |
r = http_requests.post(base_url, headers=CMS_HEADERS_POST(), json=post_data)
|
| 484 |
if r.status_code in [200, 201]:
|
|
|
|
| 655 |
article_id = data.get("article_id", "")
|
| 656 |
name = data.get("name", "わせます太郎").strip() or "わせます太郎"
|
| 657 |
body = data.get("body", "").strip()
|
| 658 |
+
parent_id = data.get("parent_id", "")
|
| 659 |
if not body:
|
| 660 |
return JSONResponse({"success": False, "error": "コメントを入力してください"})
|
| 661 |
+
ok = post_comment_api(article_id, name, body, parent_id)
|
| 662 |
return JSONResponse({"success": ok})
|
| 663 |
+
|
| 664 |
+
@app.post("/api/like/{article_id}")
|
| 665 |
+
async def api_like(article_id: str):
|
| 666 |
+
article = get_article(article_id)
|
| 667 |
+
if not article:
|
| 668 |
+
return JSONResponse({"success": False, "likes": 0})
|
| 669 |
+
current = int(article.get("likes", 0) or 0)
|
| 670 |
+
new_count = current + 1
|
| 671 |
+
r = http_requests.patch(cms_url(f"blog/{article_id}"), headers=CMS_HEADERS_POST(),
|
| 672 |
+
json={"likes": new_count})
|
| 673 |
+
return JSONResponse({"success": r.status_code in [200, 201], "likes": new_count})
|
templates/blog_detail.html
CHANGED
|
@@ -20,6 +20,43 @@
|
|
| 20 |
<meta name="twitter:title" content="{{ og_title }}">
|
| 21 |
{% endif %}
|
| 22 |
{% endblock %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
{% block body %}
|
| 24 |
<div class="max-w-2xl mx-auto px-4 py-6">
|
| 25 |
<a href="/" class="inline-block mb-4 text-sm text-gray-500 dark:text-gray-400 hover:text-[#bf283a] dark:hover:text-[#f08090] transition">← 一覧に戻る</a>
|
|
@@ -29,12 +66,13 @@
|
|
| 29 |
{% else %}
|
| 30 |
{% set ec = article.get("eyecatch") %}
|
| 31 |
{% set ec_url = ec.get("url","") if ec is mapping else (ec or "") %}
|
|
|
|
| 32 |
{% if ec_url %}
|
| 33 |
<img src="{{ ec_url }}" alt="" class="w-full h-64 object-cover rounded-xl mb-6">
|
| 34 |
{% endif %}
|
| 35 |
|
| 36 |
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-2">{{ article.get("title","無題") }}</h1>
|
| 37 |
-
<p class="text-sm text-gray-400 dark:text-gray-500 mb-3">👤 {{ article.get("author","") }} | 📅 {{ article.get("publishedAt","")[:10] }} | <a href="#comments" class="hover:text-[#bf283a] dark:hover:text-[#f08090] transition">💬 {{
|
| 38 |
<div class="mb-4">
|
| 39 |
{% for t in tags %}<span class="tag-badge">#{{ t }}</span>{% endfor %}
|
| 40 |
</div>
|
|
@@ -43,9 +81,18 @@
|
|
| 43 |
<div class="blog-content text-gray-800 dark:text-gray-200">{{ content | safe }}</div>
|
| 44 |
<hr class="my-6 border-gray-200 dark:border-gray-700">
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
<!-- シェア -->
|
| 47 |
{% if space_url %}
|
| 48 |
-
{% set share_link = space_url ~ "?id=" ~
|
| 49 |
{% set share_text = article.get("title","") ~ " | わせます9期ブログ\n" ~ share_link %}
|
| 50 |
<div class="bg-gray-50 dark:bg-gray-800 rounded-xl p-4 mb-6 border dark:border-gray-700">
|
| 51 |
<p class="text-sm font-bold text-gray-600 dark:text-gray-300 mb-3">🔗 この記事をシェア���る</p>
|
|
@@ -73,25 +120,19 @@
|
|
| 73 |
<h2 class="text-lg font-bold text-gray-700 dark:text-gray-200 mb-4">💬 コメント</h2>
|
| 74 |
{% if comments %}
|
| 75 |
{% for c in comments %}
|
| 76 |
-
|
| 77 |
-
<div class="flex justify-between items-center mb-1">
|
| 78 |
-
<span class="font-bold text-sm text-gray-700 dark:text-gray-200">{{ c.get("name","名無し") }}</span>
|
| 79 |
-
<span class="text-xs text-gray-400 dark:text-gray-500">{{ c.get("createdAt","") | to_jst }}</span>
|
| 80 |
-
</div>
|
| 81 |
-
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed">{{ c.get("body","") }}</p>
|
| 82 |
-
</div>
|
| 83 |
{% endfor %}
|
| 84 |
{% else %}
|
| 85 |
<p class="text-sm text-gray-400 dark:text-gray-500 mb-4">まだコメントはありません。</p>
|
| 86 |
{% endif %}
|
| 87 |
|
| 88 |
-
<div class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl p-4">
|
| 89 |
<h3 class="font-bold text-sm text-gray-700 dark:text-gray-200 mb-3">コメントを投稿</h3>
|
| 90 |
<input id="comment-name" type="text" placeholder="名前(省略可)" maxlength="50"
|
| 91 |
class="w-full border dark:border-gray-600 rounded-lg px-3 py-2 text-sm mb-2 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#e09099]">
|
| 92 |
<textarea id="comment-body" placeholder="コメントを入力..." maxlength="500" rows="3"
|
| 93 |
class="w-full border dark:border-gray-600 rounded-lg px-3 py-2 text-sm mb-3 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#e09099] resize-none"></textarea>
|
| 94 |
-
<button onclick="submitComment('{{
|
| 95 |
class="w-full bg-[#bf283a] text-white py-2 rounded-lg text-sm font-bold hover:bg-[#a01f2f] transition">投稿する</button>
|
| 96 |
<p id="comment-msg" class="text-sm text-center mt-2 hidden"></p>
|
| 97 |
</div>
|
|
@@ -99,22 +140,59 @@
|
|
| 99 |
{% endif %}
|
| 100 |
</div>
|
| 101 |
<script>
|
| 102 |
-
async function submitComment(articleId) {
|
| 103 |
-
const
|
| 104 |
-
const
|
| 105 |
-
const
|
| 106 |
-
|
|
|
|
|
|
|
| 107 |
const res = await fetch('/api/comment', {
|
| 108 |
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 109 |
-
body: JSON.stringify({article_id: articleId, name, body})
|
| 110 |
});
|
| 111 |
const data = await res.json();
|
| 112 |
if (data.success) { location.reload(); }
|
| 113 |
-
else { showMsg(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
}
|
|
|
|
| 115 |
function showMsg(el, text, cls) {
|
| 116 |
el.textContent = text; el.className = `text-sm text-center mt-2 ${cls}`; el.classList.remove('hidden');
|
| 117 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
</script>
|
| 119 |
{% if article and 'twitter-tweet' in content %}
|
| 120 |
<script async src="https://platform.twitter.com/widgets.js"></script>
|
|
|
|
| 20 |
<meta name="twitter:title" content="{{ og_title }}">
|
| 21 |
{% endif %}
|
| 22 |
{% endblock %}
|
| 23 |
+
|
| 24 |
+
{% macro render_comment(c, article_id, depth) %}
|
| 25 |
+
<div class="{% if depth > 0 %}ml-5 pl-3 border-l-2 border-pink-100 dark:border-gray-600{% endif %} mb-3">
|
| 26 |
+
<div class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl p-4">
|
| 27 |
+
<div class="flex justify-between items-center mb-1">
|
| 28 |
+
<span class="font-bold text-sm text-gray-700 dark:text-gray-200">{{ c.get("name","名無し") }}</span>
|
| 29 |
+
<span class="text-xs text-gray-400 dark:text-gray-500">{{ c.get("createdAt","") | to_jst }}</span>
|
| 30 |
+
</div>
|
| 31 |
+
<p class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed whitespace-pre-wrap">{{ c.get("body","") }}</p>
|
| 32 |
+
<button onclick="toggleReplyForm('{{ c.id }}')"
|
| 33 |
+
class="text-xs text-gray-400 dark:text-gray-500 mt-2 hover:text-[#bf283a] dark:hover:text-[#f08090] transition">
|
| 34 |
+
↩ 返信する
|
| 35 |
+
</button>
|
| 36 |
+
<div id="reply-{{ c.id }}" class="hidden mt-3 space-y-2">
|
| 37 |
+
<input id="reply-name-{{ c.id }}" type="text" placeholder="名前(省略可)" maxlength="50"
|
| 38 |
+
class="w-full border dark:border-gray-600 rounded-lg px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#e09099]">
|
| 39 |
+
<textarea id="reply-body-{{ c.id }}" placeholder="返信を入力..." maxlength="500" rows="2"
|
| 40 |
+
class="w-full border dark:border-gray-600 rounded-lg px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#e09099] resize-none"></textarea>
|
| 41 |
+
<p id="reply-msg-{{ c.id }}" class="text-sm hidden"></p>
|
| 42 |
+
<div class="flex gap-2">
|
| 43 |
+
<button onclick="submitComment('{{ article_id }}', '{{ c.id }}')"
|
| 44 |
+
class="flex-1 bg-[#bf283a] text-white py-1.5 rounded-lg text-sm font-bold hover:bg-[#a01f2f] transition">
|
| 45 |
+
返信する
|
| 46 |
+
</button>
|
| 47 |
+
<button onclick="toggleReplyForm('{{ c.id }}')"
|
| 48 |
+
class="px-3 border dark:border-gray-600 text-gray-500 dark:text-gray-400 py-1.5 rounded-lg text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition">
|
| 49 |
+
キャンセル
|
| 50 |
+
</button>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
{% for reply in c.replies %}
|
| 55 |
+
{{ render_comment(reply, article_id, depth + 1) }}
|
| 56 |
+
{% endfor %}
|
| 57 |
+
</div>
|
| 58 |
+
{% endmacro %}
|
| 59 |
+
|
| 60 |
{% block body %}
|
| 61 |
<div class="max-w-2xl mx-auto px-4 py-6">
|
| 62 |
<a href="/" class="inline-block mb-4 text-sm text-gray-500 dark:text-gray-400 hover:text-[#bf283a] dark:hover:text-[#f08090] transition">← 一覧に戻る</a>
|
|
|
|
| 66 |
{% else %}
|
| 67 |
{% set ec = article.get("eyecatch") %}
|
| 68 |
{% set ec_url = ec.get("url","") if ec is mapping else (ec or "") %}
|
| 69 |
+
{% set article_id = article.get("id","") %}
|
| 70 |
{% if ec_url %}
|
| 71 |
<img src="{{ ec_url }}" alt="" class="w-full h-64 object-cover rounded-xl mb-6">
|
| 72 |
{% endif %}
|
| 73 |
|
| 74 |
<h1 class="text-2xl font-bold text-gray-800 dark:text-gray-100 mb-2">{{ article.get("title","無題") }}</h1>
|
| 75 |
+
<p class="text-sm text-gray-400 dark:text-gray-500 mb-3">👤 {{ article.get("author","") }} | 📅 {{ article.get("publishedAt","")[:10] }} | <a href="#comments" class="hover:text-[#bf283a] dark:hover:text-[#f08090] transition">💬 {{ comment_count }}件</a></p>
|
| 76 |
<div class="mb-4">
|
| 77 |
{% for t in tags %}<span class="tag-badge">#{{ t }}</span>{% endfor %}
|
| 78 |
</div>
|
|
|
|
| 81 |
<div class="blog-content text-gray-800 dark:text-gray-200">{{ content | safe }}</div>
|
| 82 |
<hr class="my-6 border-gray-200 dark:border-gray-700">
|
| 83 |
|
| 84 |
+
<!-- いいね -->
|
| 85 |
+
<div class="flex justify-center mb-8">
|
| 86 |
+
<button id="like-btn" onclick="likeArticle('{{ article_id }}')"
|
| 87 |
+
class="flex flex-col items-center gap-1 px-8 py-4 rounded-2xl border-2 border-[#e09099] dark:border-[#c07080] hover:bg-[#fdf0f2] dark:hover:bg-[#3d1a20] transition group">
|
| 88 |
+
<span id="like-icon" class="text-3xl transition-transform group-hover:scale-110">♡</span>
|
| 89 |
+
<span id="like-count" class="text-sm font-bold text-[#bf283a] dark:text-[#f08090]">{{ article.get("likes", 0) or 0 }}</span>
|
| 90 |
+
</button>
|
| 91 |
+
</div>
|
| 92 |
+
|
| 93 |
<!-- シェア -->
|
| 94 |
{% if space_url %}
|
| 95 |
+
{% set share_link = space_url ~ "?id=" ~ article_id %}
|
| 96 |
{% set share_text = article.get("title","") ~ " | わせます9期ブログ\n" ~ share_link %}
|
| 97 |
<div class="bg-gray-50 dark:bg-gray-800 rounded-xl p-4 mb-6 border dark:border-gray-700">
|
| 98 |
<p class="text-sm font-bold text-gray-600 dark:text-gray-300 mb-3">🔗 この記事をシェア���る</p>
|
|
|
|
| 120 |
<h2 class="text-lg font-bold text-gray-700 dark:text-gray-200 mb-4">💬 コメント</h2>
|
| 121 |
{% if comments %}
|
| 122 |
{% for c in comments %}
|
| 123 |
+
{{ render_comment(c, article_id, 0) }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
{% endfor %}
|
| 125 |
{% else %}
|
| 126 |
<p class="text-sm text-gray-400 dark:text-gray-500 mb-4">まだコメントはありません。</p>
|
| 127 |
{% endif %}
|
| 128 |
|
| 129 |
+
<div class="bg-white dark:bg-gray-800 border dark:border-gray-700 rounded-xl p-4 mt-4">
|
| 130 |
<h3 class="font-bold text-sm text-gray-700 dark:text-gray-200 mb-3">コメントを投稿</h3>
|
| 131 |
<input id="comment-name" type="text" placeholder="名前(省略可)" maxlength="50"
|
| 132 |
class="w-full border dark:border-gray-600 rounded-lg px-3 py-2 text-sm mb-2 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#e09099]">
|
| 133 |
<textarea id="comment-body" placeholder="コメントを入力..." maxlength="500" rows="3"
|
| 134 |
class="w-full border dark:border-gray-600 rounded-lg px-3 py-2 text-sm mb-3 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-[#e09099] resize-none"></textarea>
|
| 135 |
+
<button onclick="submitComment('{{ article_id }}')"
|
| 136 |
class="w-full bg-[#bf283a] text-white py-2 rounded-lg text-sm font-bold hover:bg-[#a01f2f] transition">投稿する</button>
|
| 137 |
<p id="comment-msg" class="text-sm text-center mt-2 hidden"></p>
|
| 138 |
</div>
|
|
|
|
| 140 |
{% endif %}
|
| 141 |
</div>
|
| 142 |
<script>
|
| 143 |
+
async function submitComment(articleId, parentId = '') {
|
| 144 |
+
const nameEl = parentId ? document.getElementById(`reply-name-${parentId}`) : document.getElementById('comment-name');
|
| 145 |
+
const bodyEl = parentId ? document.getElementById(`reply-body-${parentId}`) : document.getElementById('comment-body');
|
| 146 |
+
const msgEl = parentId ? document.getElementById(`reply-msg-${parentId}`) : document.getElementById('comment-msg');
|
| 147 |
+
const name = nameEl.value.trim();
|
| 148 |
+
const body = bodyEl.value.trim();
|
| 149 |
+
if (!body) { showMsg(msgEl, 'コメントを入力してください', 'text-red-500'); return; }
|
| 150 |
const res = await fetch('/api/comment', {
|
| 151 |
method: 'POST', headers: {'Content-Type': 'application/json'},
|
| 152 |
+
body: JSON.stringify({article_id: articleId, name, body, parent_id: parentId})
|
| 153 |
});
|
| 154 |
const data = await res.json();
|
| 155 |
if (data.success) { location.reload(); }
|
| 156 |
+
else { showMsg(msgEl, '投稿に失敗しました', 'text-red-500'); }
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
function toggleReplyForm(commentId) {
|
| 160 |
+
const el = document.getElementById(`reply-${commentId}`);
|
| 161 |
+
el.classList.toggle('hidden');
|
| 162 |
+
if (!el.classList.contains('hidden')) {
|
| 163 |
+
el.querySelector('textarea').focus();
|
| 164 |
+
}
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
async function likeArticle(articleId) {
|
| 168 |
+
const key = `liked_${articleId}`;
|
| 169 |
+
if (localStorage.getItem(key)) return;
|
| 170 |
+
const btn = document.getElementById('like-btn');
|
| 171 |
+
btn.disabled = true;
|
| 172 |
+
const res = await fetch(`/api/like/${articleId}`, { method: 'POST' });
|
| 173 |
+
const data = await res.json();
|
| 174 |
+
if (data.success) {
|
| 175 |
+
localStorage.setItem(key, '1');
|
| 176 |
+
document.getElementById('like-icon').textContent = '❤️';
|
| 177 |
+
document.getElementById('like-count').textContent = data.likes;
|
| 178 |
+
btn.classList.add('bg-[#fdf0f2]', 'dark:bg-[#3d1a20]');
|
| 179 |
+
}
|
| 180 |
+
btn.disabled = false;
|
| 181 |
}
|
| 182 |
+
|
| 183 |
function showMsg(el, text, cls) {
|
| 184 |
el.textContent = text; el.className = `text-sm text-center mt-2 ${cls}`; el.classList.remove('hidden');
|
| 185 |
}
|
| 186 |
+
|
| 187 |
+
// ページ読み込み時:すでにいいね済みなら見た目を反映
|
| 188 |
+
(function() {
|
| 189 |
+
const articleId = '{{ article.get("id","") if article else "" }}';
|
| 190 |
+
if (articleId && localStorage.getItem(`liked_${articleId}`)) {
|
| 191 |
+
document.getElementById('like-icon').textContent = '❤️';
|
| 192 |
+
const btn = document.getElementById('like-btn');
|
| 193 |
+
if (btn) btn.classList.add('bg-[#fdf0f2]', 'dark:bg-[#3d1a20]');
|
| 194 |
+
}
|
| 195 |
+
})();
|
| 196 |
</script>
|
| 197 |
{% if article and 'twitter-tweet' in content %}
|
| 198 |
<script async src="https://platform.twitter.com/widgets.js"></script>
|