Skip to content

Commit e1018ca

Browse files
Merge branch 'dev'
2 parents 1ce4f43 + 0643c0b commit e1018ca

7 files changed

Lines changed: 120 additions & 33 deletions

File tree

‎apps/codebility/app/home/_components/LeftSidebar.tsx‎

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,6 @@ const LeftSidebar = () => {
5454
links: section.links.filter((link: SidebarLink) => {
5555
// TODO: Remove these filters when features are implemented
5656

57-
// Filter out Feeds from MENU section
58-
if (link.label === "Feeds") {
59-
console.log("🚧 Feeds feature not yet implemented - hiding from navigation");
60-
return false;
61-
}
62-
6357
// Filter out Codev Overflow from CODEVS section
6458
if (link.label === "Codev Overflow") {
6559
console.log("🚧 Codev Overflow feature not yet implemented - hiding from navigation");
@@ -69,13 +63,13 @@ const LeftSidebar = () => {
6963
return true; // Keep all other links
7064
})
7165
}));
72-
66+
7367
setSidebarData(filteredData);
7468
}
7569
};
7670

7771
fetchSidebarData();
78-
}, [roleId, refreshKey]);
72+
}, []);
7973

8074
const sidebarVariants = {
8175
closed: {

‎apps/codebility/app/home/_components/LeftSidebarClient.tsx‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,7 @@ const LeftSidebarClient = ({ initialSidebarData }: LeftSidebarClientProps) => {
4141
const filteredSidebarData = initialSidebarData.map((section) => ({
4242
...section,
4343
links: section.links.filter((link) => {
44-
// TODO: Remove these filters when features are implemented
45-
if (link.label === "Feeds" || link.label === "Codev Overflow") {
46-
return false;
47-
}
44+
4845
return true;
4946
})
5047
}));

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ const CreatePostForm = ({
7171
setImage(null);
7272
} catch (error) {
7373
console.error("Error creating post:", error);
74+
75+
if (error instanceof Error) {
76+
toast.error(error.message);
77+
return;
78+
}
79+
7480
toast.error("Failed to create post.");
7581
} finally {
7682
setIsSubmitting(false);

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ const EditPostForm = ({
8383
setImage(null);
8484
} catch (error) {
8585
console.error("Error editing post:", error);
86+
87+
if (error instanceof Error) {
88+
toast.error(error.message);
89+
return;
90+
}
91+
8692
toast.error("Failed to edit post.");
8793
} finally {
8894
setIsSubmitting(false);

‎apps/codebility/app/home/feeds/_services/action.ts‎

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"use server";
22
import { createClientServerComponent } from "@/utils/supabase/server";
33
import { deleteImage, getImagePath } from "@/utils/uploadImage";
4+
import { createPostSchema, editPostSchema } from "./validation";
5+
import { ZodError } from "zod";
46

57

68
export const getUserRole = async (userId: Number | null): Promise<string | null> => {
@@ -43,15 +45,22 @@ type AddPostParams = {
4345
tag_ids?: number[] | null;
4446
};
4547

46-
export const addPost = async ({
47-
title,
48-
content,
49-
author_id,
50-
image_url,
51-
content_image_ids,
52-
tag_ids,
53-
}: AddPostParams) => {
48+
export const addPost = async (
49+
payload
50+
: AddPostParams) => {
5451
try {
52+
// Validation
53+
const parsed = createPostSchema.parse(payload);
54+
55+
const {
56+
title,
57+
content,
58+
author_id,
59+
image_url,
60+
content_image_ids,
61+
tag_ids,
62+
} = parsed;
63+
5564
const supabase = await createClientServerComponent();
5665

5766
// 1. Insert the post
@@ -105,6 +114,10 @@ export const addPost = async ({
105114
return newPost;
106115
} catch (error) {
107116
console.error("Error in addPost:", error);
117+
118+
if (error instanceof ZodError) {
119+
throw new Error(error.errors[0]?.message ?? "Invalid post data");
120+
}
108121
throw error;
109122
}
110123
};
@@ -155,16 +168,22 @@ type EditPostParams = {
155168
tag_ids?: number[] | null;
156169
};
157170

158-
export const editPost = async ({
159-
id,
160-
title,
161-
content,
162-
author_id,
163-
image_url,
164-
content_image_ids,
165-
tag_ids,
166-
}: EditPostParams) => {
171+
export const editPost = async (payload: EditPostParams) => {
167172
try {
173+
// Validation
174+
const parsed = editPostSchema.parse(payload);
175+
176+
const {
177+
id,
178+
title,
179+
content,
180+
author_id,
181+
image_url,
182+
content_image_ids,
183+
tag_ids,
184+
} = parsed;
185+
186+
168187
const supabase = await createClientServerComponent();
169188

170189
// 1. Fetch existing post for old image cleanup
@@ -255,6 +274,9 @@ export const editPost = async ({
255274
return updatedPost;
256275
} catch (error) {
257276
console.error("Error editing post:", error);
277+
if (error instanceof ZodError) {
278+
throw new Error(error.errors[0]?.message ?? "Invalid post data");
279+
}
258280
throw error;
259281
}
260282
};
@@ -553,3 +575,29 @@ export async function deletePostComment(comment_id: string) {
553575
throw error;
554576
}
555577
}
578+
579+
export const hasReachedDailyPostLimit = async (author_id: string, limit = 2) => {
580+
try {
581+
const supabase = await createClientServerComponent();
582+
583+
// Timestamp 24 hours ago
584+
const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
585+
586+
const { count, error } = await supabase
587+
.from("posts")
588+
.select("id", { count: "exact", head: true }) // head: true avoids fetching rows
589+
.eq("author_id", author_id)
590+
.gte("created_at", twentyFourHoursAgo);
591+
592+
if (error) {
593+
console.error("Error checking daily post limit:", error);
594+
throw error;
595+
}
596+
597+
// Return true if count >= limit
598+
return (count ?? 0) >= limit;
599+
} catch (error) {
600+
console.error("Error in hasReachedDailyPostLimit:", error);
601+
throw error;
602+
}
603+
};
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { z } from "zod";
2+
3+
export const createPostSchema = z.object({
4+
title: z
5+
.string()
6+
.min(3, "Title must be at least 3 characters")
7+
.max(150, "Title is too long"),
8+
9+
content: z
10+
.string()
11+
.max(15000, "Post content is too long"),
12+
13+
author_id: z.string().nullable().optional(),
14+
image_url: z.string().url().nullable().optional(),
15+
content_image_ids: z.any().optional(),
16+
tag_ids: z.any().optional(),
17+
});
18+
19+
20+
export const editPostSchema = createPostSchema.extend({
21+
id: z.string(),
22+
});

‎apps/codebility/app/home/feeds/page.tsx‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { useEffect, useState } from "react";
44
import CreatePostModal from "@/components/modals/CreatePostModal";
55
import { Button } from "@/components/ui/button";
66
import { useUserStore } from "@/store/codev-store";
7-
import { ChevronDown } from "lucide-react";
87

98
import Feed from "./_components/Feed";
109
import SocialPointsCard from "./_components/SocialPointsCard";
1110
import SortMenu from "./_components/SortMenu";
1211
import { SEARCH_DEBOUNCE_MS } from "./_constants";
13-
import { getUserRole } from "./_services/action";
12+
import { getUserRole, hasReachedDailyPostLimit } from "./_services/action";
13+
import toast from "react-hot-toast";
1414

1515
type SortField = "title" | "date" | "upvotes";
1616
type SortOrder = "asc" | "desc";
@@ -22,7 +22,6 @@ export default function FeedsPage() {
2222
const [debouncedQuery, setDebouncedQuery] = useState("");
2323
const [sortField, setSortField] = useState<SortField>("date");
2424
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
25-
const [sortMenuOpen, setSortMenuOpen] = useState(false);
2625
const { user } = useUserStore();
2726

2827
useEffect(() => {
@@ -42,7 +41,22 @@ export default function FeedsPage() {
4241
return () => clearTimeout(handler);
4342
}, [searchQuery]);
4443

45-
const openModal = () => setIsModalOpen(true);
44+
const openModal = async () => {
45+
if (!user) return;
46+
47+
try {
48+
const unallowed = await hasReachedDailyPostLimit(user.id);
49+
50+
if (unallowed) {
51+
toast.error("Daily post limit reached. You’re not allowed to create a post right now.");
52+
return;
53+
}
54+
55+
setIsModalOpen(true);
56+
} catch (error) {
57+
console.error("Failed to check posting permission", error);
58+
}
59+
};
4660
const closeModal = () => setIsModalOpen(false);
4761

4862
return (

0 commit comments

Comments
 (0)