Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/docs/public/assets/schools/self-host.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions apps/docs/src/pages/en/self-hosting/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ You should self host CourseLit, if you:
- want complete control of your data
- want to host it behind a firewall for internal use

## How to self host?

![Self hosting options](/assets/schools/self-host.svg)

### Self host CourseLit

See [the self hosting guide](/en/self-hosting/self-host).
Expand Down
18 changes: 13 additions & 5 deletions apps/web/app/api/media/presigned/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { NextRequest } from "next/server";
import { responses } from "@/config/strings";
import * as medialitService from "@/services/medialit";
import { UIConstants as constants } from "@courselit/common-models";
import { checkPermission } from "@courselit/utils";
import User from "@models/User";
import DomainModel, { Domain } from "@models/Domain";
import { auth } from "@/auth";
import { error } from "@/services/logger";
import { MediaLit } from "medialit";

const medialit = new MediaLit({
apiKey: process.env.MEDIALIT_APIKEY,
endpoint: process.env.MEDIALIT_SERVER,
});

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

try {
let response = await medialitService.getPresignedUrlForUpload(
domain.name,
);
return Response.json({ url: response });
let signature = await medialit.getSignature({
group: domain.name,
});
return Response.json({
signature,
endpoint: medialit.endpoint,
});
} catch (err: any) {
error(err.message, {
stack: err.stack,
Expand Down
162 changes: 119 additions & 43 deletions apps/web/components/community/create-post-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import { useState, useEffect, useContext } from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Expand All @@ -22,22 +21,41 @@ import { Paperclip, Video, Smile, Image } from "lucide-react";
import { EmojiPicker } from "./emoji-picker";
import { GifSelector } from "./gif-selector";
import { MediaPreview } from "./media-preview";
import { CommunityPost } from "@courselit/common-models";
import { CommunityMediaTypes, CommunityPost } from "@courselit/common-models";
import { type MediaItem } from "./media-item";
import { ProfileContext } from "@components/contexts";
import {
AlertDialog,
AlertDialogContent,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@components/ui/alert-dialog";
import {
AlertDialogAction,
AlertDialogCancel,
} from "@radix-ui/react-alert-dialog";
import { Progress } from "@/components/ui/progress";

interface CreatePostDialogProps {
onPostCreated: (
createPost: (
post: Pick<CommunityPost, "title" | "content" | "category"> & {
media: MediaItem[];
},
) => void;
categories: string[];
isFileUploading: boolean;
fileUploadProgress: number;
fileBeingUploadedNumber: number;
}

export function CreatePostDialog({
onPostCreated,
export default function CreatePostDialog({
createPost,
categories,
isFileUploading,
fileUploadProgress,
fileBeingUploadedNumber = 0,
}: CreatePostDialogProps) {
const [isOpen, setIsOpen] = useState(false);
const [title, setTitle] = useState("");
Expand All @@ -53,10 +71,13 @@ export function CreatePostDialog({
}>({});
const [isPostButtonDisabled, setIsPostButtonDisabled] = useState(true);
const { profile } = useContext(ProfileContext);
const [isPosting, setIsPosting] = useState(false);

useEffect(() => {
setIsPostButtonDisabled(title.trim() === "" || content.trim() === "");
}, [title, content]);
setIsPostButtonDisabled(
title.trim() === "" || content.trim() === "" || isPosting,
);
}, [title, content, isPosting]);

const handleEmojiSelect = (emoji: string) => {
setContent((prevContent) => prevContent + emoji);
Expand Down Expand Up @@ -98,9 +119,9 @@ export function CreatePostDialog({
}
};

const handleLinkAdd = (url: string) => {
setContent((prevContent) => `${prevContent} ${url} `);
};
// const handleLinkAdd = (url: string) => {
// setContent((prevContent) => `${prevContent} ${url} `);
// };

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

const handlePost = () => {
const handlePost = async () => {
if (title.trim() === "" || content.trim() === "") {
setErrors({
title: title.trim() === "" ? "Title is required" : undefined,
Expand All @@ -145,61 +166,87 @@ export function CreatePostDialog({
return;
}

onPostCreated({
setIsPosting(true);
await createPost({
category,
title,
content,
media,
});
setIsPosting(false);

resetForm();
};

const resetForm = () => {
setIsOpen(false);
// Reset form
setTitle("");
setContent("");
setCategory("");
setMedia([]);
setErrors({});
};

const getUploadableMediaCount = () => {
return media.filter((x) =>
[
CommunityMediaTypes.IMAGE,
CommunityMediaTypes.VIDEO,
CommunityMediaTypes.PDF,
].includes(x.type as any),
).length;
};

if (!profile) {
return null;
}

return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<AlertDialog open={isOpen}>
<AlertDialogTrigger asChild>
<Button
variant="outline"
className="w-full !text-left cursor-text"
onClick={() => setIsOpen(true)}
>
Write something...
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[90vw] md:max-w-[600px] w-full overflow-y-auto max-h-[calc(100vh-4rem)] my-8">
<div className="flex items-center gap-2 mb-4">
<Avatar className="h-10 w-10">
<AvatarImage
src={
profile.avatar
? profile.avatar.file
: "/courselit_backdrop_square.webp"
}
alt={`${profile.name} avatar`}
/>
<AvatarFallback>
{(profile.name
? profile.name.charAt(0)
: profile.email.charAt(0)
).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<span className="font-semibold">{profile.name}</span>
</div>
</div>
</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-[90vw] md:max-w-[600px] w-full overflow-y-auto max-h-[calc(100vh-4rem)] my-8">
<AlertDialogHeader>
<AlertDialogTitle>
<div className="flex items-center gap-2 mb-4">
<Avatar className="h-10 w-10">
<AvatarImage
src={
profile.avatar
? profile.avatar.file
: "/courselit_backdrop_square.webp"
}
alt={`${profile.name} avatar`}
/>
<AvatarFallback>
{(profile.name
? profile.name.charAt(0)
: profile.email!.charAt(0)
).toUpperCase()}
</AvatarFallback>
</Avatar>
<div>
<span className="font-semibold">
{profile.name}
</span>
</div>
</div>
</AlertDialogTitle>
</AlertDialogHeader>

<div className="space-y-4">
<div>
<Input
placeholder="Title"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="text-lg border-none px-0 font-semibold"
/>
{errors.title && (
<p className="text-red-500 text-sm mt-1">
Expand Down Expand Up @@ -409,7 +456,7 @@ export function CreatePostDialog({
)}
</div>
</div>
<div className="flex items-center gap-2">
{/* <div className="flex items-center gap-2">
<Button
variant="ghost"
onClick={() => setIsOpen(false)}
Expand All @@ -423,9 +470,38 @@ export function CreatePostDialog({
>
Post
</Button>
</div>
</div> */}
</div>
</DialogContent>
</Dialog>
{isPosting && getUploadableMediaCount() > 0 && (
<>
<p className="text-xs text-muted-foreground">
Uploading {fileBeingUploadedNumber} of{" "}
{getUploadableMediaCount()} files -{" "}
{Math.round(fileUploadProgress)}%
</p>
<Progress value={fileUploadProgress} className="h-2" />
</>
)}
<AlertDialogFooter>
<AlertDialogCancel asChild>
<Button
onClick={resetForm}
variant="outline"
disabled={isPosting}
>
Cancel
</Button>
</AlertDialogCancel>
<AlertDialogAction asChild>
<Button
onClick={handlePost}
disabled={isPostButtonDisabled}
>
{isPosting ? "Posting..." : "Post"}
</Button>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
Loading
Loading