Skip to content

Commit bff3e63

Browse files
rajat1saxenaRajat
andauthored
Editable community posts (#735)
* bug fix: Invite customer email trimming * Editable posts * Codex issues fixed --------- Co-authored-by: Rajat <hi@rajatsaxena.dev>
1 parent 476b0d3 commit bff3e63

20 files changed

Lines changed: 2795 additions & 592 deletions
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import React from "react";
2+
import {
3+
render,
4+
screen,
5+
fireEvent,
6+
waitFor,
7+
act,
8+
} from "@testing-library/react";
9+
import CreatePostDialog from "../create-post-dialog";
10+
import { ProfileContext } from "@components/contexts";
11+
12+
jest.mock("../../ui/button", () => ({
13+
Button: ({ children, ...props }: any) => (
14+
<button {...props}>{children}</button>
15+
),
16+
}));
17+
18+
jest.mock("../../ui/input", () => ({
19+
Input: (props: any) => <input {...props} />,
20+
}));
21+
22+
jest.mock("../../ui/textarea", () => ({
23+
Textarea: (props: any) => <textarea {...props} />,
24+
}));
25+
26+
jest.mock("../../ui/progress", () => ({
27+
Progress: ({ value }: { value: number }) => (
28+
<div data-testid="progress">{value}</div>
29+
),
30+
}));
31+
32+
jest.mock("../../ui/avatar", () => ({
33+
Avatar: ({ children }: any) => <div>{children}</div>,
34+
AvatarFallback: ({ children }: any) => <div>{children}</div>,
35+
AvatarImage: (props: any) => <img {...props} alt={props.alt || "avatar"} />,
36+
}));
37+
38+
jest.mock("../../ui/dialog", () => ({
39+
Dialog: ({ children }: any) => <div>{children}</div>,
40+
DialogContent: ({ children }: any) => <div>{children}</div>,
41+
DialogHeader: ({ children }: any) => <div>{children}</div>,
42+
DialogTitle: ({ children }: any) => <div>{children}</div>,
43+
DialogTrigger: ({ children }: any) => <>{children}</>,
44+
DialogFooter: ({ children }: any) => <div>{children}</div>,
45+
DialogClose: ({ children }: any) => <>{children}</>,
46+
}));
47+
48+
jest.mock("../../ui/popover", () => ({
49+
Popover: ({ children }: any) => <div>{children}</div>,
50+
PopoverTrigger: ({ children }: any) => <>{children}</>,
51+
PopoverContent: ({ children }: any) => <div>{children}</div>,
52+
}));
53+
54+
jest.mock("../../ui/select", () => ({
55+
Select: ({ value, onValueChange, children }: any) => (
56+
<select
57+
aria-label="category"
58+
value={value}
59+
onChange={(e) => onValueChange(e.target.value)}
60+
>
61+
<option value="">Select a category</option>
62+
{children}
63+
</select>
64+
),
65+
SelectContent: ({ children }: any) => <>{children}</>,
66+
SelectItem: ({ value, children }: any) => (
67+
<option value={value}>{children}</option>
68+
),
69+
SelectTrigger: ({ children }: any) => <>{children}</>,
70+
SelectValue: () => null,
71+
}));
72+
73+
jest.mock("../emoji-picker", () => ({
74+
EmojiPicker: () => <div>EmojiPicker</div>,
75+
}));
76+
77+
jest.mock("../gif-selector", () => ({
78+
GifSelector: () => <div>GifSelector</div>,
79+
}));
80+
81+
jest.mock("../media-preview", () => ({
82+
MediaPreview: () => <div>MediaPreview</div>,
83+
}));
84+
85+
const renderDialog = (
86+
createPost: any,
87+
overrides: Partial<React.ComponentProps<typeof CreatePostDialog>> = {},
88+
) =>
89+
render(
90+
<ProfileContext.Provider
91+
value={{
92+
profile: {
93+
name: "Test User",
94+
email: "test@example.com",
95+
avatar: undefined,
96+
},
97+
setProfile: jest.fn(),
98+
}}
99+
>
100+
<CreatePostDialog
101+
isOpen={true}
102+
onOpenChange={jest.fn()}
103+
createPost={createPost}
104+
categories={["General", "Announcements"]}
105+
isFileUploading={false}
106+
fileUploadProgress={0}
107+
fileBeingUploadedNumber={0}
108+
{...overrides}
109+
/>
110+
</ProfileContext.Provider>,
111+
);
112+
113+
const flushMicrotasks = async () => {
114+
await act(async () => {
115+
await Promise.resolve();
116+
});
117+
};
118+
119+
describe("CreatePostDialog", () => {
120+
it("keeps submit disabled until title, content and category are present", async () => {
121+
const createPost = jest.fn().mockResolvedValue(undefined);
122+
123+
renderDialog(createPost);
124+
await flushMicrotasks();
125+
126+
const postButton = screen.getByRole("button", { name: "Post" });
127+
expect(postButton).toBeDisabled();
128+
129+
fireEvent.change(screen.getByPlaceholderText("Title"), {
130+
target: { value: "My title" },
131+
});
132+
fireEvent.change(screen.getByPlaceholderText("What's on your mind?"), {
133+
target: { value: "My content" },
134+
});
135+
expect(postButton).toBeDisabled();
136+
137+
fireEvent.change(screen.getByLabelText("category"), {
138+
target: { value: "General" },
139+
});
140+
141+
await waitFor(() => {
142+
expect(postButton).toBeEnabled();
143+
});
144+
});
145+
146+
it("shows posting state while submit is pending and restores after completion", async () => {
147+
let resolveSubmit: (() => void) | undefined;
148+
const createPost = jest.fn().mockImplementation(
149+
() =>
150+
new Promise<void>((resolve) => {
151+
resolveSubmit = resolve;
152+
}),
153+
);
154+
155+
renderDialog(createPost, { category: "General" });
156+
await flushMicrotasks();
157+
158+
fireEvent.change(screen.getByPlaceholderText("Title"), {
159+
target: { value: "My title" },
160+
});
161+
fireEvent.change(screen.getByPlaceholderText("What's on your mind?"), {
162+
target: { value: "My content" },
163+
});
164+
fireEvent.change(screen.getByLabelText("category"), {
165+
target: { value: "General" },
166+
});
167+
168+
await waitFor(() => {
169+
expect(screen.getByRole("button", { name: "Post" })).toBeEnabled();
170+
});
171+
172+
fireEvent.click(screen.getByRole("button", { name: "Post" }));
173+
174+
await waitFor(() => {
175+
expect(createPost).toHaveBeenCalledTimes(1);
176+
});
177+
expect(
178+
screen.getByRole("button", { name: "Posting..." }),
179+
).toBeDisabled();
180+
181+
await act(async () => {
182+
resolveSubmit?.();
183+
});
184+
185+
await waitFor(() => {
186+
expect(screen.getByRole("button", { name: "Post" })).toBeDisabled();
187+
});
188+
});
189+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from "react";
2+
import { render, screen } from "@testing-library/react";
3+
import { MediaPreview } from "../media-preview";
4+
5+
jest.mock("../../ui/button", () => ({
6+
Button: ({ children, ...props }: any) => (
7+
<button {...props}>{children}</button>
8+
),
9+
}));
10+
11+
jest.mock("../../ui/scroll-area", () => ({
12+
ScrollArea: ({ children }: any) => <div>{children}</div>,
13+
ScrollBar: () => null,
14+
}));
15+
16+
describe("MediaPreview", () => {
17+
it("renders image using media thumbnail when url is missing", () => {
18+
render(
19+
<MediaPreview
20+
items={[
21+
{
22+
type: "image",
23+
title: "CourseLit Notification System.png",
24+
media: {
25+
mediaId: "m1",
26+
thumbnail: "https://cdn.example.com/thumb.png",
27+
file: "https://cdn.example.com/file.png",
28+
},
29+
} as any,
30+
]}
31+
onRemove={jest.fn()}
32+
/>,
33+
);
34+
35+
const image = screen.getByAltText(
36+
"CourseLit Notification System.png",
37+
) as HTMLImageElement;
38+
expect(image).toBeInTheDocument();
39+
expect(image.src).toContain("https://cdn.example.com/thumb.png");
40+
});
41+
42+
it("renders image using url when available", () => {
43+
render(
44+
<MediaPreview
45+
items={[
46+
{
47+
type: "image",
48+
title: "Local",
49+
url: "blob:http://localhost/abc",
50+
} as any,
51+
]}
52+
onRemove={jest.fn()}
53+
/>,
54+
);
55+
56+
const image = screen.getByAltText("Local") as HTMLImageElement;
57+
expect(image).toBeInTheDocument();
58+
expect(image.getAttribute("src")).toBe("blob:http://localhost/abc");
59+
});
60+
});

apps/web/components/community/comment-section.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ export default function CommentSection({
107107
mediaId
108108
file
109109
thumbnail
110+
size
110111
}
111112
}
112113
likesCount
@@ -183,6 +184,7 @@ export default function CommentSection({
183184
mediaId
184185
file
185186
thumbnail
187+
size
186188
}
187189
}
188190
likesCount
@@ -278,6 +280,7 @@ export default function CommentSection({
278280
mediaId
279281
file
280282
thumbnail
283+
size
281284
}
282285
}
283286
likesCount
@@ -364,6 +367,7 @@ export default function CommentSection({
364367
mediaId
365368
file
366369
thumbnail
370+
size
367371
}
368372
}
369373
likesCount
@@ -517,6 +521,7 @@ export default function CommentSection({
517521
mediaId
518522
file
519523
thumbnail
524+
size
520525
}
521526
}
522527
likesCount

apps/web/components/community/comment.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,11 +194,7 @@ export function Comment({
194194
{comment.user.name}
195195
</span>
196196
<span className="text-xs text-muted-foreground">
197-
{formattedLocaleDate(
198-
comment.updatedAt
199-
? new Date(comment.updatedAt).getTime()
200-
: undefined,
201-
)}
197+
{formattedLocaleDate(comment.updatedAt)}
202198
</span>
203199
</div>
204200
{!comment.deleted && (

0 commit comments

Comments
 (0)