Skip to content

Commit a904b46

Browse files
Created a system post in the feeds page.
1 parent 242c451 commit a904b46

6 files changed

Lines changed: 125 additions & 28 deletions

File tree

‎apps/codebility/app/home/feeds/_components/Feed.tsx‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState } from "react";
44
import { useFeedsStore } from "@/store/feeds-store";
55

66
import { POSTS_PER_PAGE } from "../_constants";
7+
import { SYSTEM_POST } from "../_constants/system-post";
78
import Post from "./PostCard";
89
import PostCardSkeleton from "./PostCardSkeleton";
910

@@ -31,11 +32,20 @@ export default function Feed({
3132
fetchPosts();
3233
}, []);
3334

35+
// Always prepend system post to regular posts
36+
const allPosts = useMemo(() => {
37+
return [SYSTEM_POST, ...posts];
38+
}, [posts]);
39+
3440
// Filter posts
3541
const filteredPosts = useMemo(() => {
36-
if (!searchQuery || searchQuery.trim() === "") return posts;
42+
if (!searchQuery || searchQuery.trim() === "") return allPosts;
43+
3744
const query = searchQuery.toLowerCase();
38-
return posts.filter((post) => {
45+
return allPosts.filter((post) => {
46+
// Always include system post
47+
if (post.id === "00000000-0000-0000-0000-000000000001") return true;
48+
3949
const titleMatch = post.title?.toLowerCase().includes(query);
4050
const firstNameMatch = post.author_id?.first_name
4151
?.toLowerCase()
@@ -45,12 +55,16 @@ export default function Feed({
4555
.includes(query);
4656
return titleMatch || firstNameMatch || lastNameMatch;
4757
});
48-
}, [posts, searchQuery]);
58+
}, [allPosts, searchQuery]);
4959

5060
// Sort posts
5161
const sortedPosts = useMemo(() => {
5262
const sorted = [...filteredPosts];
5363
sorted.sort((a, b) => {
64+
// Always keep system post at the top regardless of sorting
65+
if (a.id === "00000000-0000-0000-0000-000000000001") return -1;
66+
if (b.id === "00000000-0000-0000-0000-000000000001") return 1;
67+
5468
let aVal: string | number = "";
5569
let bVal: string | number = "";
5670

@@ -106,7 +120,11 @@ export default function Feed({
106120
if (currentLoader) observer.unobserve(currentLoader);
107121
};
108122
}, [sortedPosts.length]);
123+
109124
const handleDeletePost = (deletedPostId: string | number) => {
125+
// Prevent deletion of system post
126+
if (deletedPostId === "00000000-0000-0000-0000-000000000001") return;
127+
110128
useFeedsStore.setState((state) => ({
111129
posts: state.posts.filter((post) => post.id !== deletedPostId),
112130
}));
@@ -149,4 +167,4 @@ export default function Feed({
149167
)}
150168
</>
151169
);
152-
}
170+
}

‎apps/codebility/app/home/feeds/_components/PostCard.tsx‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { PostType } from "../_services/query";
1414
import { DeleteDialog } from "./DeleteDialog";
1515
import PostCommentCount from "./PostCommentCount";
1616
import PostTags from "./PostTags";
17-
import PostUpvote from "./PostUpvote";
17+
import PostUpvote from "./PostUpvote";
1818

1919
interface PostProps {
2020
post: PostType;
@@ -42,7 +42,8 @@ export default function Post({ post, isAdmin, onDelete }: PostProps) {
4242
if (user) {
4343
checkIfAuthor();
4444
}
45-
}, [user]);
45+
}, [user, post.author_id?.id]);
46+
4647
const openDeleteDialog = async (e: React.MouseEvent) => {
4748
e.stopPropagation();
4849
e.preventDefault();
@@ -68,14 +69,15 @@ export default function Post({ post, isAdmin, onDelete }: PostProps) {
6869

6970
const openModal = () => setIsModalOpen(true);
7071
const closeModal = () => setIsModalOpen(false);
72+
7173
return (
7274
<div>
7375
<Box
7476
className="group relative flex h-[440px] cursor-pointer flex-col justify-between rounded-xl p-3"
7577
onClick={openModal}
7678
>
7779
{/* Delete Button in top-right */}
78-
{(isAdmin || isAuthor) && (
80+
{(isAdmin || isAuthor) && post.id !== "00000000-0000-0000-0000-000000000001" && (
7981
<button
8082
className="invisible absolute right-2 top-2 rounded bg-red-500 px-2 py-1 text-white hover:bg-red-600 group-hover:visible"
8183
onClick={openDeleteDialog}
@@ -89,15 +91,18 @@ export default function Post({ post, isAdmin, onDelete }: PostProps) {
8991
<div className="flex items-center justify-between">
9092
<div className="mb-2 flex items-center">
9193
<Image
92-
src={post.author_id?.image_url || defaultAvatar}
93-
alt={`${post.author_id?.first_name}'s profile`}
94+
src={
95+
post.author_id?.image_url ||
96+
(post.id === "00000000-0000-0000-0000-000000000001" ? "/favicon.ico" : defaultAvatar)
97+
}
98+
alt={`${post.author_id?.first_name || "Codebility"}'s profile`}
9499
className="mr-4 h-12 w-12 rounded-full object-cover"
95-
width={40}
96-
height={40}
100+
width={30}
101+
height={30}
97102
/>
98103
<div className="flex-1">
99104
<h2 className="font-semibold text-gray-800 dark:text-gray-100">
100-
{post.author_id
105+
{post.author_id?.first_name
101106
? `${post.author_id?.first_name} ${post.author_id?.last_name}`
102107
: "Codebility"}
103108
</h2>
@@ -146,4 +151,4 @@ export default function Post({ post, isAdmin, onDelete }: PostProps) {
146151
/>
147152
</div>
148153
);
149-
}
154+
}

‎apps/codebility/app/home/feeds/_components/PostView.tsx‎

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import ReactMarkdown from "react-markdown";
1414
import remarkGfm from "remark-gfm";
1515

1616
import type { PostType } from "../_services/query";
17+
import { SYSTEM_POST } from "../_constants/system-post";
1718
import PostUpvote from "../_components/PostUpvote";
1819
import { getUserRole } from "../_services/action";
1920
import PostCommentCount from "./PostCommentCount";
@@ -40,10 +41,14 @@ export default function PostView({ postId }: PostViewProps) {
4041
setRefreshComments((prev) => prev + 1);
4142
};
4243

43-
// Find the post in the store
44+
// Find the post in the store or use system post
4445
useEffect(() => {
45-
const foundPost = posts.find((p) => p.id === postId) || null;
46-
setPost(foundPost);
46+
if (postId === "00000000-0000-0000-0000-000000000001") {
47+
setPost(SYSTEM_POST);
48+
} else {
49+
const foundPost = posts.find((p) => p.id === postId) || null;
50+
setPost(foundPost);
51+
}
4752
}, [posts, postId]);
4853

4954
// Check roles
@@ -83,15 +88,18 @@ export default function PostView({ postId }: PostViewProps) {
8388
<h1 className="text-3xl font-bold">{post.title}</h1>
8489
<div className="flex items-center">
8590
<Image
86-
src={post.author_id?.image_url || defaultAvatar}
87-
alt={`${post.author_id?.first_name || "User"}'s profile`}
91+
src={
92+
post.author_id?.image_url ||
93+
(post.id === "00000000-0000-0000-0000-000000000001" ? "/favicon.ico" : defaultAvatar)
94+
}
95+
alt={`${post.author_id?.first_name || "Codebility"}'s profile`}
8896
className="mr-4 h-10 w-10 rounded-full object-cover"
8997
width={40}
9098
height={40}
9199
/>
92100
<div className="flex-1">
93101
<h2 className="font-semibold text-gray-800 dark:text-gray-100">
94-
{post.author_id
102+
{post.author_id?.first_name
95103
? `${post.author_id.first_name} ${post.author_id.last_name}`
96104
: "Codebility"}
97105
</h2>
@@ -109,19 +117,19 @@ export default function PostView({ postId }: PostViewProps) {
109117
<PostTags post={post} />
110118

111119
<div className="max-h-[90vh] overflow-y-auto">
112-
<div className="prose prose-sm dark:prose-invert max-w-none text-sm">
120+
<div className="prose prose-sm dark:prose-invert max-w-none text-sm pb-6">
113121
<ReactMarkdown remarkPlugins={[remarkGfm]}>
114-
{`![Post image](${post.image_url || "/assets/images/bg-certificate.png"})\n\n${post.content}`}
122+
{`![Post image](${post.image_url || "/assets/images/system-post-feed.png"})\n\n${post.content}`}
115123
</ReactMarkdown>
116124
</div>
117125

118-
{(isAdmin || isAuthor) && (
126+
{(isAdmin || isAuthor) && post.id !== "00000000-0000-0000-0000-000000000001" && (
119127
<Button className="mb-4 mt-4 w-auto" onClick={openModal}>
120128
Edit Post
121129
</Button>
122130
)}
123131

124-
<div className="mb-1 mt-1 border-t border-gray-200 dark:border-gray-700" />
132+
<div className="mb-1 mt-1 border-t border-gray-200 dark:border-gray-700 pt-3"/>
125133
<PostViewCommentList
126134
postId={postId}
127135
refresh={refreshComments}
@@ -141,4 +149,4 @@ export default function PostView({ postId }: PostViewProps) {
141149
/>
142150
</>
143151
);
144-
}
152+
}

‎apps/codebility/app/home/feeds/_components/PostViewCommentList.tsx‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,10 @@ export default function PostViewCommentList({
3333
const fetchComments = async () => {
3434
try {
3535
const data = await getPostComments(postId);
36-
setComments(data);
36+
setComments(data || []); // Add fallback to empty array
3737
} catch (error) {
3838
console.error("Error fetching comments:", error);
39+
setComments([]); // Set empty array on error
3940
} finally {
4041
setLoading(false);
4142
}
@@ -52,7 +53,7 @@ export default function PostViewCommentList({
5253
);
5354
}
5455

55-
if (comments.length === 0) {
56+
if (!comments || comments.length === 0) {
5657
return (
5758
<p className="text-sm text-gray-500 dark:text-gray-400">
5859
No comments yet.
@@ -71,10 +72,10 @@ export default function PostViewCommentList({
7172
userName={`${c.commenter?.first_name} ${c.commenter?.last_name}`}
7273
content={c.content}
7374
userCanDelete={
74-
user?.id == c.commenter?.id || hasDeleteCommentPrivilege
75+
user?.id === c.commenter?.id || hasDeleteCommentPrivilege
7576
}
7677
/>
7778
))}
7879
</div>
7980
);
80-
}
81+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { PostType } from "../_services/query";
2+
3+
export const SYSTEM_POST: PostType = {
4+
id: "00000000-0000-0000-0000-000000000001",
5+
title: "Join the Codebility Feeds! 🚀",
6+
content: `Welcome to the Codebility Community Feed — the central space where developers connect, share ideas, and stay informed.
7+
8+
---
9+
10+
## 🧠 What This Feed Is For
11+
12+
This feed is built for two core purposes:
13+
14+
📢 **Organization-wide announcements**
15+
💬 **Community-driven posts from members**
16+
17+
Whether you're sharing an update, a helpful tip, a question, a learning resource, or something cool you're building — this is your space.
18+
19+
---
20+
21+
## ⭐ Earn as You Contribute
22+
23+
Your activity here is rewarded:
24+
25+
📝 Earn points for every post you create
26+
❤️ Gain more points when your posts receive likes and comments
27+
28+
The more value you bring to the community, the more you grow within it.
29+
30+
---
31+
32+
## ⏱ Posting Guidelines
33+
34+
To keep the feed high-quality and meaningful:
35+
36+
✓ Each member can create **one post per day**
37+
✓ Make it count — share something useful, inspiring, or thought-provoking
38+
39+
---
40+
41+
## 🚀 Build Together
42+
43+
This feed isn't just a timeline — it's a shared space to learn, collaborate, and grow as developers.
44+
45+
**Welcome to the community.**
46+
We're excited to see what you'll share!
47+
48+
**-Codebility Team**
49+
50+
`,
51+
image_url: "/assets/images/system-post-feed.png",
52+
created_at: "2026-02-11T00:00:00.000Z",
53+
author_id: {
54+
id: "00000000-0000-0000-0000-000000000000",
55+
first_name: "Codebility",
56+
last_name: "",
57+
image_url: "/favicon.ico",
58+
},
59+
upvote_count: 0,
60+
comment_count: 0,
61+
tags: [
62+
{ tag_id: "system-1", name: "New Release" },
63+
{ tag_id: "system-2", name: "Announcement" },
64+
],
65+
};
717 KB
Loading

0 commit comments

Comments
 (0)