Skip to content

Commit e929d63

Browse files
rajat1saxenaRajat Saxena
andauthored
Large file uploads (#658)
* Large file uploads Fixes #657 * Text editor image upload ported to new medialit api * Ported community posts and text editor uploads to new MediaLit API * CodeQL fixes --------- Co-authored-by: Rajat Saxena <[email protected]>
1 parent 57a9da5 commit e929d63

File tree

22 files changed

+1017
-416
lines changed

22 files changed

+1017
-416
lines changed

apps/docs/public/assets/schools/self-host.svg

Lines changed: 3 additions & 0 deletions
Loading

apps/docs/src/pages/en/self-hosting/introduction.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ You should self host CourseLit, if you:
1515
- want complete control of your data
1616
- want to host it behind a firewall for internal use
1717

18+
## How to self host?
19+
20+
![Self hosting options](/assets/schools/self-host.svg)
21+
1822
### Self host CourseLit
1923

2024
See [the self hosting guide](/en/self-hosting/self-host).

apps/web/app/api/media/presigned/route.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
import { NextRequest } from "next/server";
22
import { responses } from "@/config/strings";
3-
import * as medialitService from "@/services/medialit";
43
import { UIConstants as constants } from "@courselit/common-models";
54
import { checkPermission } from "@courselit/utils";
65
import User from "@models/User";
76
import DomainModel, { Domain } from "@models/Domain";
87
import { auth } from "@/auth";
98
import { error } from "@/services/logger";
9+
import { MediaLit } from "medialit";
10+
11+
const medialit = new MediaLit({
12+
apiKey: process.env.MEDIALIT_APIKEY,
13+
endpoint: process.env.MEDIALIT_SERVER,
14+
});
1015

1116
export async function POST(req: NextRequest) {
1217
const domain = await DomainModel.findOne<Domain>({
@@ -41,10 +46,13 @@ export async function POST(req: NextRequest) {
4146
}
4247

4348
try {
44-
let response = await medialitService.getPresignedUrlForUpload(
45-
domain.name,
46-
);
47-
return Response.json({ url: response });
49+
let signature = await medialit.getSignature({
50+
group: domain.name,
51+
});
52+
return Response.json({
53+
signature,
54+
endpoint: medialit.endpoint,
55+
});
4856
} catch (err: any) {
4957
error(err.message, {
5058
stack: err.stack,

apps/web/components/community/create-post-dialog.tsx

Lines changed: 119 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import { useState, useEffect, useContext } from "react";
44
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
55
import { Button } from "@/components/ui/button";
6-
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
76
import { Input } from "@/components/ui/input";
87
import { Textarea } from "@/components/ui/textarea";
98
import {
@@ -22,22 +21,41 @@ import { Paperclip, Video, Smile, Image } from "lucide-react";
2221
import { EmojiPicker } from "./emoji-picker";
2322
import { GifSelector } from "./gif-selector";
2423
import { MediaPreview } from "./media-preview";
25-
import { CommunityPost } from "@courselit/common-models";
24+
import { CommunityMediaTypes, CommunityPost } from "@courselit/common-models";
2625
import { type MediaItem } from "./media-item";
2726
import { ProfileContext } from "@components/contexts";
27+
import {
28+
AlertDialog,
29+
AlertDialogContent,
30+
AlertDialogFooter,
31+
AlertDialogHeader,
32+
AlertDialogTitle,
33+
AlertDialogTrigger,
34+
} from "@components/ui/alert-dialog";
35+
import {
36+
AlertDialogAction,
37+
AlertDialogCancel,
38+
} from "@radix-ui/react-alert-dialog";
39+
import { Progress } from "@/components/ui/progress";
2840

2941
interface CreatePostDialogProps {
30-
onPostCreated: (
42+
createPost: (
3143
post: Pick<CommunityPost, "title" | "content" | "category"> & {
3244
media: MediaItem[];
3345
},
3446
) => void;
3547
categories: string[];
48+
isFileUploading: boolean;
49+
fileUploadProgress: number;
50+
fileBeingUploadedNumber: number;
3651
}
3752

38-
export function CreatePostDialog({
39-
onPostCreated,
53+
export default function CreatePostDialog({
54+
createPost,
4055
categories,
56+
isFileUploading,
57+
fileUploadProgress,
58+
fileBeingUploadedNumber = 0,
4159
}: CreatePostDialogProps) {
4260
const [isOpen, setIsOpen] = useState(false);
4361
const [title, setTitle] = useState("");
@@ -53,10 +71,13 @@ export function CreatePostDialog({
5371
}>({});
5472
const [isPostButtonDisabled, setIsPostButtonDisabled] = useState(true);
5573
const { profile } = useContext(ProfileContext);
74+
const [isPosting, setIsPosting] = useState(false);
5675

5776
useEffect(() => {
58-
setIsPostButtonDisabled(title.trim() === "" || content.trim() === "");
59-
}, [title, content]);
77+
setIsPostButtonDisabled(
78+
title.trim() === "" || content.trim() === "" || isPosting,
79+
);
80+
}, [title, content, isPosting]);
6081

6182
const handleEmojiSelect = (emoji: string) => {
6283
setContent((prevContent) => prevContent + emoji);
@@ -98,9 +119,9 @@ export function CreatePostDialog({
98119
}
99120
};
100121

101-
const handleLinkAdd = (url: string) => {
102-
setContent((prevContent) => `${prevContent} ${url} `);
103-
};
122+
// const handleLinkAdd = (url: string) => {
123+
// setContent((prevContent) => `${prevContent} ${url} `);
124+
// };
104125

105126
const handleVideoAdd = (url: string) => {
106127
if (url.includes("youtube.com") || url.includes("youtu.be")) {
@@ -127,7 +148,7 @@ export function CreatePostDialog({
127148
setMedia((prevMedia) => prevMedia.filter((_, i) => i !== index));
128149
};
129150

130-
const handlePost = () => {
151+
const handlePost = async () => {
131152
if (title.trim() === "" || content.trim() === "") {
132153
setErrors({
133154
title: title.trim() === "" ? "Title is required" : undefined,
@@ -145,61 +166,87 @@ export function CreatePostDialog({
145166
return;
146167
}
147168

148-
onPostCreated({
169+
setIsPosting(true);
170+
await createPost({
149171
category,
150172
title,
151173
content,
152174
media,
153175
});
176+
setIsPosting(false);
177+
178+
resetForm();
179+
};
180+
181+
const resetForm = () => {
154182
setIsOpen(false);
155-
// Reset form
156183
setTitle("");
157184
setContent("");
158185
setCategory("");
159186
setMedia([]);
160187
setErrors({});
161188
};
162189

190+
const getUploadableMediaCount = () => {
191+
return media.filter((x) =>
192+
[
193+
CommunityMediaTypes.IMAGE,
194+
CommunityMediaTypes.VIDEO,
195+
CommunityMediaTypes.PDF,
196+
].includes(x.type as any),
197+
).length;
198+
};
199+
200+
if (!profile) {
201+
return null;
202+
}
203+
163204
return (
164-
<Dialog open={isOpen} onOpenChange={setIsOpen}>
165-
<DialogTrigger asChild>
205+
<AlertDialog open={isOpen}>
206+
<AlertDialogTrigger asChild>
166207
<Button
167208
variant="outline"
168209
className="w-full !text-left cursor-text"
210+
onClick={() => setIsOpen(true)}
169211
>
170212
Write something...
171213
</Button>
172-
</DialogTrigger>
173-
<DialogContent className="sm:max-w-[90vw] md:max-w-[600px] w-full overflow-y-auto max-h-[calc(100vh-4rem)] my-8">
174-
<div className="flex items-center gap-2 mb-4">
175-
<Avatar className="h-10 w-10">
176-
<AvatarImage
177-
src={
178-
profile.avatar
179-
? profile.avatar.file
180-
: "/courselit_backdrop_square.webp"
181-
}
182-
alt={`${profile.name} avatar`}
183-
/>
184-
<AvatarFallback>
185-
{(profile.name
186-
? profile.name.charAt(0)
187-
: profile.email.charAt(0)
188-
).toUpperCase()}
189-
</AvatarFallback>
190-
</Avatar>
191-
<div>
192-
<span className="font-semibold">{profile.name}</span>
193-
</div>
194-
</div>
214+
</AlertDialogTrigger>
215+
<AlertDialogContent className="sm:max-w-[90vw] md:max-w-[600px] w-full overflow-y-auto max-h-[calc(100vh-4rem)] my-8">
216+
<AlertDialogHeader>
217+
<AlertDialogTitle>
218+
<div className="flex items-center gap-2 mb-4">
219+
<Avatar className="h-10 w-10">
220+
<AvatarImage
221+
src={
222+
profile.avatar
223+
? profile.avatar.file
224+
: "/courselit_backdrop_square.webp"
225+
}
226+
alt={`${profile.name} avatar`}
227+
/>
228+
<AvatarFallback>
229+
{(profile.name
230+
? profile.name.charAt(0)
231+
: profile.email!.charAt(0)
232+
).toUpperCase()}
233+
</AvatarFallback>
234+
</Avatar>
235+
<div>
236+
<span className="font-semibold">
237+
{profile.name}
238+
</span>
239+
</div>
240+
</div>
241+
</AlertDialogTitle>
242+
</AlertDialogHeader>
195243

196244
<div className="space-y-4">
197245
<div>
198246
<Input
199247
placeholder="Title"
200248
value={title}
201249
onChange={(e) => setTitle(e.target.value)}
202-
className="text-lg border-none px-0 font-semibold"
203250
/>
204251
{errors.title && (
205252
<p className="text-red-500 text-sm mt-1">
@@ -409,7 +456,7 @@ export function CreatePostDialog({
409456
)}
410457
</div>
411458
</div>
412-
<div className="flex items-center gap-2">
459+
{/* <div className="flex items-center gap-2">
413460
<Button
414461
variant="ghost"
415462
onClick={() => setIsOpen(false)}
@@ -423,9 +470,38 @@ export function CreatePostDialog({
423470
>
424471
Post
425472
</Button>
426-
</div>
473+
</div> */}
427474
</div>
428-
</DialogContent>
429-
</Dialog>
475+
{isPosting && getUploadableMediaCount() > 0 && (
476+
<>
477+
<p className="text-xs text-muted-foreground">
478+
Uploading {fileBeingUploadedNumber} of{" "}
479+
{getUploadableMediaCount()} files -{" "}
480+
{Math.round(fileUploadProgress)}%
481+
</p>
482+
<Progress value={fileUploadProgress} className="h-2" />
483+
</>
484+
)}
485+
<AlertDialogFooter>
486+
<AlertDialogCancel asChild>
487+
<Button
488+
onClick={resetForm}
489+
variant="outline"
490+
disabled={isPosting}
491+
>
492+
Cancel
493+
</Button>
494+
</AlertDialogCancel>
495+
<AlertDialogAction asChild>
496+
<Button
497+
onClick={handlePost}
498+
disabled={isPostButtonDisabled}
499+
>
500+
{isPosting ? "Posting..." : "Post"}
501+
</Button>
502+
</AlertDialogAction>
503+
</AlertDialogFooter>
504+
</AlertDialogContent>
505+
</AlertDialog>
430506
);
431507
}

0 commit comments

Comments
 (0)