diff --git a/devplacepy/content.py b/devplacepy/content.py index c225409c..0df59d13 100644 --- a/devplacepy/content.py +++ b/devplacepy/content.py @@ -60,6 +60,21 @@ REACTABLE_TYPES = {"post", "comment", "gist", "project", "quiz"} logger = logging.getLogger(__name__) +def get_project_by_uid(project_uid: str | None) -> dict | None: + if not project_uid: + return None + project = get_table("projects").find_one(uid=project_uid) + if not project: + return None + slug = project.get("slug") or project["uid"] + return { + "uid": project["uid"], + "name": project.get("title") or project.get("name", ""), + "slug": slug, + "url": f"/projects/{slug}", + } + + def is_owner(item: dict | None, user: dict | None) -> bool: return bool(item and user and item["user_uid"] == user["uid"]) @@ -512,6 +527,7 @@ def detail_context( "reactions": detail.get("reactions", {"counts": {}, "mine": []}), "bookmarked": detail.get("bookmarked", False), "poll": detail.get("poll"), + "project_link": detail.get("project_link"), } if extra: context.update(extra) @@ -691,6 +707,7 @@ def load_detail( "reactions": reactions, "bookmarked": bookmarked, "poll": get_poll_for_post(item["uid"], user) if target_type == "post" else None, + "project_link": get_project_by_uid(item.get("project_uid")) if target_type == "post" else None, } @@ -718,6 +735,8 @@ def enrich_items( entry[name] = ( source(item) if callable(source) else source.get(item["uid"], 0) ) + if key == "post" and item.get("project_uid"): + entry["project_link"] = get_project_by_uid(item["project_uid"]) enriched.append(entry) return enriched diff --git a/devplacepy/routers/devrant/rants.py b/devplacepy/routers/devrant/rants.py index 0695d474..0b979a16 100644 --- a/devplacepy/routers/devrant/rants.py +++ b/devplacepy/routers/devrant/rants.py @@ -87,6 +87,7 @@ async def create_rant(request: Request): if len(text) > 125000: return dr_error("Your rant is too long.") tags = _parse_tags(params.get("tags")) + project_uid = params.get("project_uid") or None uid, slug = create_content_item( "posts", "post", @@ -95,7 +96,7 @@ async def create_rant(request: Request): "title": None, "content": text, "topic": "rant", - "project_uid": None, + "project_uid": project_uid, "image": None, "tags": encode_tags(tags), }, diff --git a/devplacepy/schemas/content.py b/devplacepy/schemas/content.py index 5e851c87..fe370a4e 100644 --- a/devplacepy/schemas/content.py +++ b/devplacepy/schemas/content.py @@ -72,6 +72,13 @@ class BadgeOut(_Out): model_config = ConfigDict(populate_by_name=True) +class ProjectLinkOut(_Out): + uid: str = "" + name: Optional[str] = None + slug: Optional[str] = None + url: Optional[str] = None + + class PostOut(_Out): uid: str = "" slug: Optional[str] = None @@ -81,6 +88,7 @@ class PostOut(_Out): topic: Optional[str] = None stars: Optional[int] = None image: Optional[str] = None + project_uid: Optional[str] = None created_at: Optional[str] = None updated_at: Optional[str] = None diff --git a/devplacepy/schemas/listings.py b/devplacepy/schemas/listings.py index ecbab0b7..eaa5afdb 100644 --- a/devplacepy/schemas/listings.py +++ b/devplacepy/schemas/listings.py @@ -14,6 +14,7 @@ from devplacepy.schemas.content import ( NotificationOut, PollOut, PostOut, + ProjectLinkOut, ProjectOut, ReactionsOut, UserOut, @@ -31,6 +32,7 @@ class FeedItemOut(_Out): reactions: ReactionsOut = ReactionsOut() bookmarked: bool = False poll: Optional[PollOut] = None + project_link: Optional[ProjectLinkOut] = None class GistItemOut(_Out): @@ -132,6 +134,7 @@ class PostDetailOut(_Out): comment_count: Optional[int] = None related_posts: list[FeedItemOut] = [] topics: list[str] = [] + project_link: Optional[ProjectLinkOut] = None class ProjectsOut(_Out): diff --git a/devplacepy/services/devrant/serializers.py b/devplacepy/services/devrant/serializers.py index 6b16ecbd..2878509d 100644 --- a/devplacepy/services/devrant/serializers.py +++ b/devplacepy/services/devrant/serializers.py @@ -4,6 +4,7 @@ import json from typing import Optional from devplacepy.avatar import avatar_seed +from devplacepy.database import get_table from devplacepy.services.devrant.avatar import avatar_payload from devplacepy.services.devrant.ids import to_unix @@ -26,6 +27,22 @@ def encode_tags(tags: list) -> str: return json.dumps(cleaned) +def _rant_project(post: dict) -> dict | None: + project_uid = post.get("project_uid") + if not project_uid: + return None + project = get_table("projects").find_one(uid=project_uid) + if not project: + return None + slug = project.get("slug") or project["uid"] + return { + "uid": project["uid"], + "name": project.get("title") or project.get("name", ""), + "slug": slug, + "url": f"/projects/{slug}", + } + + def rant_text(post: dict) -> str: title = (post.get("title") or "").strip() content = post.get("content") or "" @@ -56,6 +73,7 @@ def serialize_rant( author = authors.get(post["user_uid"]) or {} uid = post["uid"] username = author.get("username") or "" + project = _rant_project(post) return { "id": int(post["id"]), "text": rant_text(post), @@ -75,6 +93,8 @@ def serialize_rant( "user_avatar": avatar_payload(avatar_seed(author)), "user_avatar_lg": avatar_payload(avatar_seed(author)), "editable": bool(viewer and viewer.get("uid") == post["user_uid"]), + "project_uid": post.get("project_uid"), + "project": project, } diff --git a/devplacepy/static/css/post.css b/devplacepy/static/css/post.css index 989fb2cf..937e99b3 100644 --- a/devplacepy/static/css/post.css +++ b/devplacepy/static/css/post.css @@ -55,6 +55,22 @@ margin-bottom: 1rem; } +.project-link { + display: inline-block; + font-size: 0.875rem; + color: var(--accent); + font-weight: 600; + padding: 0.375rem 0.75rem; + margin-bottom: 0.75rem; + background: var(--bg-card-hover); + border-radius: var(--radius); + text-decoration: none; +} + +.project-link:hover { + text-decoration: underline; +} + .post-detail-actions { display: flex; align-items: center; diff --git a/devplacepy/templates/_post_card.html b/devplacepy/templates/_post_card.html index adaa9e27..8b94e56c 100644 --- a/devplacepy/templates/_post_card.html +++ b/devplacepy/templates/_post_card.html @@ -17,6 +17,10 @@