feat: add like count display on homepage and thumbs-up toggle on detail page
- Display like count with thumbs-up emoji on post cards in index.html
- Add clickable like/unlike button with JS fetch on post_detail.html
- Add POST /web/posts/{slug}/like endpoint in web routes for cookie-auth users
- Guests redirected to /auth/dev-login on 401
- Use block extra_js (matching base template) for inline script
This commit is contained in:
@@ -52,6 +52,9 @@
|
|||||||
<span class="post-card-meta-item" data-testid="post-date-{{ post.id }}">
|
<span class="post-card-meta-item" data-testid="post-date-{{ post.id }}">
|
||||||
{{ post.created_at.strftime('%B %d, %Y') }}
|
{{ post.created_at.strftime('%B %d, %Y') }}
|
||||||
</span>
|
</span>
|
||||||
|
<span class="post-card-meta-item" data-testid="like-count-{{ post.id }}">
|
||||||
|
👍 {{ post.like_count }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="post-card-content" data-testid="post-content-preview-{{ post.id }}">
|
<div class="post-card-content" data-testid="post-content-preview-{{ post.id }}">
|
||||||
|
|||||||
@@ -33,6 +33,13 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge" data-testid="post-detail-status">{{ _('post.status_draft', current_locale) }}</span>
|
<span class="badge" data-testid="post-detail-status">{{ _('post.status_draft', current_locale) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<span class="post-card-meta-item" data-testid="post-detail-like-count">
|
||||||
|
<button id="like-button" class="btn-like" data-testid="like-button"
|
||||||
|
data-post-slug="{{ post.slug }}"
|
||||||
|
data-liked="false">
|
||||||
|
👍 <span id="like-count">{{ post.like_count }}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -83,3 +90,42 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</article>
|
</article>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
<script data-testid="like-script">
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var likeButton = document.getElementById('like-button');
|
||||||
|
if (!likeButton) return;
|
||||||
|
|
||||||
|
likeButton.addEventListener('click', function() {
|
||||||
|
var slug = this.getAttribute('data-post-slug');
|
||||||
|
var countSpan = document.getElementById('like-count');
|
||||||
|
|
||||||
|
fetch('/web/posts/' + slug + '/like', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
if (response.status === 401) {
|
||||||
|
window.location.href = '/auth/dev-login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Like request failed');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(function(data) {
|
||||||
|
if (data && data.like_count !== undefined) {
|
||||||
|
countSpan.textContent = data.like_count;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.error('Like error:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ from app.application.use_cases import (
|
|||||||
GetPostUseCase,
|
GetPostUseCase,
|
||||||
ListPostsUseCase,
|
ListPostsUseCase,
|
||||||
PublishPostUseCase,
|
PublishPostUseCase,
|
||||||
|
TogglePostLikeUseCase,
|
||||||
UpdatePostUseCase,
|
UpdatePostUseCase,
|
||||||
)
|
)
|
||||||
from app.domain.exceptions import (
|
from app.domain.exceptions import (
|
||||||
@@ -523,6 +524,39 @@ async def delete_post(
|
|||||||
return RedirectResponse(url="/web/", status_code=303)
|
return RedirectResponse(url="/web/", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/posts/{post_slug}/like")
|
||||||
|
async def toggle_like_web(
|
||||||
|
post_slug: str,
|
||||||
|
user: OptionalUserDep,
|
||||||
|
get_use_case: FromDishka[GetPostUseCase],
|
||||||
|
toggle_use_case: FromDishka[TogglePostLikeUseCase],
|
||||||
|
) -> dict[str, object]:
|
||||||
|
"""Toggle like on a post via web UI.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
post_slug: The URL-friendly slug of the post.
|
||||||
|
user: Current user from cookie or None.
|
||||||
|
get_use_case: Use case for retrieving posts.
|
||||||
|
toggle_use_case: Use case for toggling likes.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON dict with updated like_count.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If post not found or user not authenticated.
|
||||||
|
"""
|
||||||
|
if not user:
|
||||||
|
raise HTTPException(status_code=401, detail="Authentication required")
|
||||||
|
|
||||||
|
try:
|
||||||
|
post = await get_use_case.by_slug(post_slug)
|
||||||
|
except NotFoundException:
|
||||||
|
raise HTTPException(status_code=404, detail="Post not found") from None
|
||||||
|
|
||||||
|
result = await toggle_use_case.execute(post.id, user.user_id)
|
||||||
|
return {"like_count": result.like_count}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/profile", response_class=HTMLResponse)
|
@router.get("/profile", response_class=HTMLResponse)
|
||||||
async def profile(
|
async def profile(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
Reference in New Issue
Block a user