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 }}">
|
||||
{{ post.created_at.strftime('%B %d, %Y') }}
|
||||
</span>
|
||||
<span class="post-card-meta-item" data-testid="like-count-{{ post.id }}">
|
||||
👍 {{ post.like_count }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="post-card-content" data-testid="post-content-preview-{{ post.id }}">
|
||||
|
||||
@@ -33,6 +33,13 @@
|
||||
{% else %}
|
||||
<span class="badge" data-testid="post-detail-status">{{ _('post.status_draft', current_locale) }}</span>
|
||||
{% 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>
|
||||
</header>
|
||||
|
||||
@@ -83,3 +90,42 @@
|
||||
</footer>
|
||||
</article>
|
||||
{% 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,
|
||||
ListPostsUseCase,
|
||||
PublishPostUseCase,
|
||||
TogglePostLikeUseCase,
|
||||
UpdatePostUseCase,
|
||||
)
|
||||
from app.domain.exceptions import (
|
||||
@@ -523,6 +524,39 @@ async def delete_post(
|
||||
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)
|
||||
async def profile(
|
||||
request: Request,
|
||||
|
||||
Reference in New Issue
Block a user