Skip to content

Commit 37165af

Browse files
authored
Merge pull request #116 from codegasms/feat/posts-backend-api
Setup backend + frontend for posts entity + unhide groups + show submissions page for contests
2 parents 9d2b5ec + 5d85116 commit 37165af

File tree

19 files changed

+2733
-16
lines changed

19 files changed

+2733
-16
lines changed

app/[orgId]/contests/[id]/page.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,25 @@ export default function ContestDetailsPage() {
245245
Problems will be available when the contest starts.
246246
</div>
247247
)}
248+
249+
{shouldShowProblems() ? (
250+
<div>
251+
<h3 className="text-lg font-semibold mb-2 flex items-center">
252+
<Link href={`/${orgId}/contests/${contestId}/submissions`}>
253+
<Button
254+
variant="link"
255+
className="p-0 h-auto text-primary hover:text-primary/80"
256+
>
257+
View Submissions
258+
</Button>
259+
</Link>
260+
</h3>
261+
</div>
262+
) : (
263+
<div className="text-muted-foreground italic">
264+
Submissions will be available when the contest starts.
265+
</div>
266+
)}
248267
</CardContent>
249268
</Card>
250269
</div>

app/[orgId]/contests/[id]/submissions/mockData.ts

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import SubmissionsPage from "@/app/[orgId]/submissions/page";
2+
3+
export default function SubmissionsPageWrapper({
4+
params,
5+
}: {
6+
params: {
7+
orgId: string;
8+
id: string;
9+
};
10+
}) {
11+
return <SubmissionsPage params={params} />;
12+
}

app/[orgId]/groups/mockData.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface Group {
66
about?: string;
77
avatar?: string;
88
users: string; // user emails seperated by newline
9+
userEmails: string[];
910
usersCount?: number;
1011
}
1112

@@ -19,6 +20,11 @@ export const mockGroups: Group[] = [
1920
avatar: "https://api.dicebear.com/7.x/initials/svg?seed=ET",
2021
users:
2122
23+
userEmails: [
24+
25+
26+
27+
],
2228
},
2329
{
2430
id: 2,
@@ -28,6 +34,7 @@ export const mockGroups: Group[] = [
2834
about: "Product design and UX team",
2935
avatar: "https://api.dicebear.com/7.x/initials/svg?seed=DT",
3036
37+
3138
},
3239
{
3340
id: 3,
@@ -38,6 +45,11 @@ export const mockGroups: Group[] = [
3845
avatar: "https://api.dicebear.com/7.x/initials/svg?seed=MT",
3946
users:
4047
48+
userEmails: [
49+
50+
51+
52+
],
4153
},
4254
{
4355
id: 4,
@@ -47,5 +59,6 @@ export const mockGroups: Group[] = [
4759
about: "Product management and strategy",
4860
avatar: "https://api.dicebear.com/7.x/initials/svg?seed=PT",
4961
62+
5063
},
5164
];

app/[orgId]/groups/page.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ const groupSchema = z.object({
4343
});
4444

4545
const injectUsersCount = (groups: Group[]) => {
46+
console.log("injecting users count", groups);
4647
return groups.map((group) => ({
4748
...group,
48-
usersCount: group.users?.split(/\r?\n/).length ?? 0,
49+
usersCount: group.userEmails.length ?? 0,
4950
}));
5051
};
5152

@@ -80,7 +81,9 @@ export default function GroupsPage() {
8081
throw new Error(formatValidationErrors(errorData));
8182
}
8283
const data = await response.json();
83-
setGroups(injectUsersCount(data));
84+
const updatedData = injectUsersCount(data);
85+
console.log("updatedData", updatedData);
86+
setGroups(updatedData);
8487
setShowMockAlert(false);
8588
} catch (error) {
8689
console.error("Error fetching groups:", error);
@@ -137,10 +140,12 @@ export default function GroupsPage() {
137140

138141
// Convert users from string to array of strings (splitting at newlines)
139142
if (typeof groupToSave.users === "string") {
140-
groupToSave.users = groupToSave.users
143+
groupToSave.emails = groupToSave.users
141144
.split("\n")
142145
.map((user) => user.trim())
143146
.filter((user) => user.length > 0); // Remove empty lines
147+
// groupToSave.users = null;
148+
console.log("groupToSave.users", groupToSave.users);
144149
}
145150

146151
console.log("groups", groupToSave);

app/[orgId]/posts/[id]/page.tsx

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"use client";
2+
3+
import { CalendarIcon, UserIcon, TagIcon } from "lucide-react";
4+
import Link from "next/link";
5+
import { useEffect, useState } from "react";
6+
import { useParams } from "next/navigation";
7+
8+
import {
9+
Card,
10+
CardContent,
11+
CardDescription,
12+
CardHeader,
13+
CardTitle,
14+
} from "@/components/ui/card";
15+
import { Button } from "@/components/ui/button";
16+
17+
interface Post {
18+
id: number;
19+
title: string;
20+
content: string;
21+
tags: string;
22+
slug: string;
23+
createdAt: string;
24+
updatedAt: string;
25+
author: {
26+
id: number;
27+
name: string;
28+
nameId: string;
29+
};
30+
}
31+
32+
// Default data as fallback
33+
const defaultPostData: Post = {
34+
id: 0,
35+
title: "Loading...",
36+
content: "Loading post content...",
37+
tags: "",
38+
slug: "",
39+
createdAt: new Date().toISOString(),
40+
updatedAt: new Date().toISOString(),
41+
author: {
42+
id: 0,
43+
name: "Loading...",
44+
nameId: "loading",
45+
},
46+
};
47+
48+
export default function PostDetailsPage() {
49+
const params = useParams();
50+
const orgId = params.orgId as string;
51+
const postId = params.id as string;
52+
53+
const [postData, setPostData] = useState<Post>(defaultPostData);
54+
const [isLoading, setIsLoading] = useState(true);
55+
const [error, setError] = useState<string | null>(null);
56+
57+
useEffect(() => {
58+
const fetchPostData = async () => {
59+
try {
60+
setIsLoading(true);
61+
const url = `/api/orgs/${orgId}/posts/${postId}`;
62+
console.log(`Fetching post data from: ${url}`);
63+
64+
const res = await fetch(url);
65+
66+
if (!res.ok) {
67+
console.error(
68+
`Failed to fetch post data: ${res.status} ${res.statusText}`,
69+
);
70+
throw new Error(`Failed to fetch post data: ${res.status}`);
71+
}
72+
73+
const data = await res.json();
74+
console.log("Successfully fetched post data:", data);
75+
setPostData(data);
76+
setError(null);
77+
} catch (err) {
78+
console.error("Error fetching post data:", err);
79+
setError(
80+
err instanceof Error ? err.message : "An unknown error occurred",
81+
);
82+
} finally {
83+
setIsLoading(false);
84+
}
85+
};
86+
87+
if (orgId && postId) {
88+
fetchPostData();
89+
}
90+
}, [orgId, postId]);
91+
92+
const formatDate = (dateString: string) => {
93+
return new Date(dateString).toLocaleString("en-US", {
94+
year: "numeric",
95+
month: "long",
96+
day: "numeric",
97+
hour: "2-digit",
98+
minute: "2-digit",
99+
timeZoneName: "short",
100+
});
101+
};
102+
103+
if (isLoading) {
104+
return <div className="container px-8 py-2">Loading post details...</div>;
105+
}
106+
107+
if (error) {
108+
return (
109+
<div className="container px-8 py-2 text-red-500">Error: {error}</div>
110+
);
111+
}
112+
113+
return (
114+
<div className="container px-8 py-2 w-3xl h-screen">
115+
<Card className="bg-background">
116+
<CardHeader>
117+
<div className="flex justify-between items-start">
118+
<div>
119+
<CardTitle className="text-2xl font-bold">
120+
{postData.title}
121+
</CardTitle>
122+
<CardDescription className="text-muted-foreground">
123+
Posted by {postData.author.name}
124+
</CardDescription>
125+
</div>
126+
</div>
127+
</CardHeader>
128+
<CardContent className="space-y-6">
129+
<div className="prose prose-sm max-w-none">
130+
{postData.content.split("\n").map((paragraph, index) => (
131+
<p key={index} className="mb-4">
132+
{paragraph}
133+
</p>
134+
))}
135+
</div>
136+
137+
<div className="flex flex-col space-y-2">
138+
<div className="flex items-center text-muted-foreground">
139+
<UserIcon className="mr-2 h-4 w-4" />
140+
<Link href={`/${orgId}/users/${postData.author.nameId}`}>
141+
<Button variant="link" className="p-0 h-auto">
142+
{postData.author.name}
143+
</Button>
144+
</Link>
145+
</div>
146+
{postData.tags && (
147+
<div className="flex items-center text-muted-foreground">
148+
<TagIcon className="mr-2 h-4 w-4" />
149+
<span>
150+
{postData.tags.split(",").map((tag, index) => (
151+
<span key={index} className="mr-2">
152+
#{tag.trim()}
153+
</span>
154+
))}
155+
</span>
156+
</div>
157+
)}
158+
<div className="flex items-center text-muted-foreground">
159+
<CalendarIcon className="mr-2 h-4 w-4" />
160+
<span>Posted on {formatDate(postData.createdAt)}</span>
161+
</div>
162+
{postData.updatedAt !== postData.createdAt && (
163+
<div className="flex items-center text-muted-foreground">
164+
<CalendarIcon className="mr-2 h-4 w-4" />
165+
<span>Updated on {formatDate(postData.updatedAt)}</span>
166+
</div>
167+
)}
168+
</div>
169+
</CardContent>
170+
</Card>
171+
</div>
172+
);
173+
}

0 commit comments

Comments
 (0)