feat: Add devlog timeline of related posts to project page #141

Merged
typosaurus merged 4 commits from typosaurus/135-add-devlog-timeline-of-related-posts-to-project-page into master 2026-07-27 00:52:08 +02:00
3 changed files with 50 additions and 1 deletions
Showing only changes of commit fda72c5afb - Show all commits
+29 -1
View File
@@ -6,6 +6,7 @@ from sqlalchemy import or_
from fastapi import Depends, APIRouter, Request from fastapi import Depends, APIRouter, Request
from devplacepy.models import ProjectForm, ProjectEditForm, ProjectFlagForm, ForkForm from devplacepy.models import ProjectForm, ProjectEditForm, ProjectFlagForm, ForkForm
from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse
from devplacepy.attachments import get_attachments_batch
from devplacepy.database import ( from devplacepy.database import (
get_table, get_table,
get_users_by_uids, get_users_by_uids,
@@ -13,6 +14,10 @@ from devplacepy.database import (
get_site_stats, get_site_stats,
get_user_votes, get_user_votes,
get_recent_comments_by_target_uids, get_recent_comments_by_target_uids,
get_reactions_by_targets,
get_user_bookmarks,
get_polls_by_post_uids,
get_project_devlog,
paginate, paginate,
text_search_clause, text_search_clause,
resolve_by_slug, resolve_by_slug,
@@ -171,7 +176,7 @@ async def projects_page(
) )
@router.get("/{project_slug}", response_class=HTMLResponse) @router.get("/{project_slug}", response_class=HTMLResponse)
async def project_detail(request: Request, project_slug: str): async def project_detail(request: Request, project_slug: str, before: str = None):
user = get_current_user(request) user = get_current_user(request)
detail = load_detail("projects", "project", project_slug, user) detail = load_detail("projects", "project", project_slug, user)
if not detail: if not detail:
@@ -216,6 +221,25 @@ async def project_detail(request: Request, project_slug: str):
if parent if parent
else None else None
) )
devlog_posts, devlog_next_cursor = get_project_devlog(
project["uid"], before=before, viewer=user
)
if devlog_posts:
post_uids = [item["post"]["uid"] for item in devlog_posts]
attachments_map = get_attachments_batch("post", post_uids)
reactions_map = get_reactions_by_targets("post", post_uids, user)
bookmark_set = (
get_user_bookmarks(user["uid"], "post", post_uids) if user else set()
)
polls_map = get_polls_by_post_uids(post_uids, user)
for item in devlog_posts:
uid = item["post"]["uid"]
item["attachments"] = attachments_map.get(uid, [])
item["reactions"] = reactions_map.get(uid, {"counts": {}, "mine": []})
item["bookmarked"] = uid in bookmark_set
item["poll"] = polls_map.get(uid)
return respond( return respond(
request, request,
"project_detail.html", "project_detail.html",
@@ -235,6 +259,8 @@ async def project_detail(request: Request, project_slug: str):
"forked_from": forked_from, "forked_from": forked_from,
"fork_count": count_forks(project["uid"]), "fork_count": count_forks(project["uid"]),
"file_count": count_files(project["uid"]), "file_count": count_files(project["uid"]),
"devlog_posts": devlog_posts,
"devlog_next_cursor": devlog_next_cursor,
}, },
), ),
model=ProjectDetailOut, model=ProjectDetailOut,
@@ -464,3 +490,5 @@ async def set_project_readonly(
request: Request, project_slug: str, data: Annotated[ProjectFlagForm, Depends(json_or_form(ProjectFlagForm))] request: Request, project_slug: str, data: Annotated[ProjectFlagForm, Depends(json_or_form(ProjectFlagForm))]
): ):
return _set_project_flag(request, project_slug, "read_only", data.value) return _set_project_flag(request, project_slug, "read_only", data.value)
+3
View File
@@ -163,6 +163,8 @@ class ProjectDetailOut(_Out):
forked_from: Optional[dict] = None forked_from: Optional[dict] = None
fork_count: int = 0 fork_count: int = 0
file_count: int = 0 file_count: int = 0
devlog_posts: list[FeedItemOut] = []
devlog_next_cursor: Optional[str] = None
class GistsOut(_Out): class GistsOut(_Out):
@@ -232,3 +234,4 @@ class LeaderboardOut(_Out):
class SavedOut(_Out): class SavedOut(_Out):
items: list[SavedItemOut] = [] items: list[SavedItemOut] = []
next_cursor: Optional[str] = None next_cursor: Optional[str] = None
+18
View File
@@ -102,6 +102,22 @@
</div> </div>
</article> </article>
<section class="project-devlog">
<h3 class="project-section-label">Devlog</h3>
{% if devlog_posts %}
{% for item in devlog_posts %}
{% set _author = item.author %}{% set _time = item.time_ago %}{% set _show_share = false %}{% set _show_comment_form = false %}{% include "_post_card.html" %}
{% endfor %}
{% if devlog_next_cursor %}
<div class="devlog-footer" style="padding-top: 1rem; text-align: center;">
<a href="?before={{ devlog_next_cursor }}" class="btn btn-secondary btn-sm"><span class="icon">&#x1F4C4;</span> Load More</a>
</div>
{% endif %}
{% else %}
<p class="empty-state">No devlog posts yet.</p>
{% endif %}
</section>
{% if is_owner %} {% if is_owner %}
{% call modal('edit-project-modal', 'Edit Project') %} {% call modal('edit-project-modal', 'Edit Project') %}
<form method="POST" action="/projects/edit/{{ project['slug'] or project['uid'] }}"> <form method="POST" action="/projects/edit/{{ project['slug'] or project['uid'] }}">
@@ -184,3 +200,5 @@ if (actions) {
} }
</script> </script>
{% endblock %} {% endblock %}