Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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