Skip to content

Commit 8d929a8

Browse files
committed
Add feature to add content and upload and view images
1 parent cbc7321 commit 8d929a8

File tree

7 files changed

+172
-31
lines changed

7 files changed

+172
-31
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
.env
1+
.env
2+
content

backend/internal/handlers/courses.go

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package handlers
22

33
import (
44
"fmt"
5+
"os"
56
"strconv"
7+
"strings"
68

79
"github.com/gofiber/fiber/v2"
810
"github.com/sandbox-science/online-learning-platform/configs/database"
@@ -83,8 +85,8 @@ func Modules(c *fiber.Ctx) error {
8385
})
8486
}
8587

86-
// Content function retrieves the content of a module
87-
func Content(c *fiber.Ctx) error {
88+
// AllContent function retrieves the content of a module
89+
func AllContent(c *fiber.Ctx) error {
8890
module_id := c.Params("module_id")
8991

9092
var module entity.Module
@@ -108,6 +110,22 @@ func Content(c *fiber.Ctx) error {
108110
})
109111
}
110112

113+
// Content function retrieves a single peice of content by id
114+
func Content(c *fiber.Ctx) error {
115+
content_id := c.Params("content_id")
116+
117+
var content entity.Content
118+
// Check if the module exists
119+
if err := database.DB.Where("id = ?", content_id).First(&content).Error; err != nil {
120+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Content not found"})
121+
}
122+
123+
return c.JSON(fiber.Map{
124+
"message": "Content successfully retrieved",
125+
"content": content,
126+
})
127+
}
128+
111129
// CreateCourse creates a course and adds it to the database.
112130
func CreateCourse(c *fiber.Ctx) error {
113131
creator_id, err := strconv.Atoi(c.Params("creator_id"))
@@ -232,8 +250,8 @@ func CreateContent(c *fiber.Ctx) error {
232250
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid module_id"})
233251
}
234252

235-
var data map[string]string
236-
if err := c.BodyParser(&data); err != nil {
253+
title := c.FormValue("title")
254+
if title == "" {
237255
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid input"})
238256
}
239257

@@ -247,20 +265,40 @@ func CreateContent(c *fiber.Ctx) error {
247265
if module.Course.CreatorID != creator_id {
248266
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User is not the course creator"})
249267
}
250-
251-
// Create content
268+
269+
// Create content database entry
252270
content := entity.Content{
253-
Title: data["title"],
271+
Title: title,
254272
Module: module,
255273
}
256274

257-
content.Path = fmt.Sprintf("content/%d/%d", module.Course.ID, content.ID)
258-
259-
// Add content to database
275+
// Create content entry
260276
if err := database.DB.Create(&content).Error; err != nil {
261277
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
262278
}
263279

280+
//Save file
281+
file, err := c.FormFile("file");
282+
if err != nil{
283+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid file"})
284+
}
285+
286+
var fileExtension = file.Filename[strings.LastIndex(file.Filename, "."):]
287+
288+
if err := os.MkdirAll(fmt.Sprintf("./content/%d/", module.Course.ID), 0777); err != nil{
289+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
290+
}
291+
292+
path := fmt.Sprintf("/%d/%s", module.Course.ID, strconv.FormatUint(uint64(content.ID), 10) + fileExtension)
293+
if err := c.SaveFile(file, "./content"+path); err != nil{
294+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
295+
}
296+
297+
// Add the path
298+
if err := database.DB.Model(&content).Update("path", path).Error; err != nil{
299+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
300+
}
301+
264302
return c.JSON(fiber.Map{
265303
"message": "Content created successfully",
266304
"content": content,

backend/internal/router/route.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ func SetupRoutes(app *fiber.App) {
8282
app.Get("/courses/:user_id", handlers.Courses)
8383
app.Get("/course/:course_id", handlers.Course)
8484
app.Get("/modules/:course_id", handlers.Modules)
85-
app.Get("/content/:module_id", handlers.Content)
85+
app.Get("/content/:content_id", handlers.Content)
86+
app.Get("/all-content/:module_id", handlers.AllContent)
8687
app.Post("/create-course/:creator_id", handlers.CreateCourse)
8788
app.Post("/create-module/:creator_id/:course_id", handlers.CreateModule)
8889
app.Post("/create-content/:creator_id/:module_id", handlers.CreateContent)

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ services:
2727
- ./backend/.env
2828
volumes:
2929
- ./backend:/app
30+
- ./content:/app/content
3031

3132
backend-test:
3233
build:
@@ -54,6 +55,7 @@ services:
5455
- WATCHPACK_POLLING=true
5556
volumes:
5657
- './frontend:/app'
58+
- ./content:/app/src/content
5759

5860
volumes:
5961
learning-platform-data:

frontend/src/components/Modal.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState } from 'react';
22

3-
export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandler }) => {
3+
export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandler, fileUploadHandler = null}) => {
44
const [showModal, setShowModal] = useState(false);
55

66
const close = () => {
@@ -37,6 +37,12 @@ export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandl
3737
<div className="fixed z-[1050] top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white p-4 rounded-lg shadow-lg border-2 border-slate-300 absolute min-w-80">
3838
<h3 className="font-semibold text-lg mb-4">{title}</h3>
3939
{fieldList}
40+
{fileUploadHandler != null ? (
41+
<div>
42+
<h1>File Upload</h1>
43+
<input type="file" onChange={fileUploadHandler}/>
44+
</div>) : null
45+
}
4046
<div className="mt-4 flex justify-end space-x-2">
4147
<button
4248
className="px-4 py-2 border border-gray-300 rounded bg-gray-200 hover:bg-gray-300"

frontend/src/components/pages/Content.js

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import React, { useEffect, useState } from 'react';
22
import { useParams } from 'react-router-dom';
33

44
export function Content() {
5-
const { courseID } = useParams();
6-
const { contentID } = useParams();
7-
const [courseInfo, setCourseInfo] = useState(null);
8-
const [error, setError] = useState(null);
9-
5+
const { courseID } = useParams();
6+
const { contentID } = useParams();
7+
const [courseInfo, setCourseInfo] = useState(null);
8+
const [contentInfo, setContentInfo] = useState(null);
9+
const [error, setError] = useState(null);
10+
1011
useEffect(() => {
11-
fetch(`http://localhost:4000/course/${courseID}`)
12+
async function fetchCourse() {
13+
fetch(`http://localhost:4000/course/${courseID}`)
1214
.then((response) => {
1315
if (!response.ok) {
1416
throw new Error('Network response was not ok');
@@ -17,11 +19,29 @@ export function Content() {
1719
})
1820
.then((data) => setCourseInfo(data.course))
1921
.catch((error) => setError(error.message));
20-
}, [courseID]);
22+
}
23+
24+
async function fetchContent() {
25+
await fetch(`http://localhost:4000/content/${contentID}`)
26+
.then((response) => {
27+
if (!response.ok) {
28+
throw new Error('Network response was not ok');
29+
}
30+
return response.json();
31+
})
32+
.then((data) => setContentInfo(data.content))
33+
.catch((error) => setError(error.message));
34+
}
35+
36+
fetchCourse();
37+
fetchContent();
38+
}, [courseID, contentID]);
2139

2240
if (error) return <p>Error: {error}</p>;
2341
if (!courseInfo) return <p>Loading...</p>;
24-
42+
43+
const images = require.context("../../content", true);
44+
2545
return (
2646
<div className="p-6">
2747
<a href={`/courses/${courseID}`}>
@@ -33,6 +53,9 @@ export function Content() {
3353
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
3454
Content id: {contentID}
3555
</div>
56+
<img src={images("." + contentInfo.path)} alt=""/>
57+
3658
</div>
59+
3760
);
3861
}

frontend/src/components/pages/Course.js

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@ export function Course() {
99
const [courseInfo, setCourseInfo] = useState(null);
1010
const [userInfo, setUserInfo] = useState(null);
1111
const [error, setError] = useState(null);
12+
const [file, setFile] = useState(null);
13+
const [newContentName, setNewContentName] = useState({
14+
title: "",
15+
});
1216
const [newModuleName, setNewModuleName] = useState({
1317
title: "",
1418
});
1519

16-
const handleChange = (e) => {
20+
const handleModuleChange = (e) => {
1721
const { name, value } = e.target;
1822
setNewModuleName({[name]: value });
1923
};
2024

2125
const handleCreateModule = async (e) =>{
22-
console.log(newModuleName)
2326
if (newModuleName.title === ""){
2427
return
2528
}
@@ -50,6 +53,44 @@ export function Course() {
5053
}
5154
};
5255

56+
const handleContentChange = (e) => {
57+
const { name, value } = e.target;
58+
setNewContentName({[name]: value });
59+
};
60+
61+
const handleContentUpload = (e) => {
62+
setFile(e.target.files[0]);
63+
}
64+
65+
const handleCreateContent = (moduleID) => async (e) =>{
66+
if (newContentName.title === "" || file === null){
67+
return
68+
}
69+
70+
const userId = Cookies.get('userId');
71+
try {
72+
const formData = new FormData();
73+
formData.append("file", file);
74+
formData.append("title", newContentName.title);
75+
const response = await fetch(`http://localhost:4000/create-content/${userId}/${moduleID}`, {
76+
method: "POST",
77+
body: formData,
78+
});
79+
80+
const data = await response.json();
81+
if (response.ok) {
82+
Notiflix.Notify.success("Content creation successful!");
83+
setTimeout(() => {
84+
window.location.reload();
85+
}, 500);
86+
} else {
87+
Notiflix.Notify.failure(data.message || "Content creation failed");
88+
}
89+
} catch (error) {
90+
Notiflix.Notify.failure("Error occurred during content creation");
91+
}
92+
};
93+
5394
useEffect(() => {
5495
const userId = Cookies.get('userId');
5596

@@ -102,16 +143,45 @@ export function Course() {
102143
</a>
103144
)
104145
})
105-
moduleList.push(
106-
<div>
107-
<div className="bg-slate-400 p-4 rounded shadow" >
108-
<h3 className="text-xl font-semibold" >{module.title}</h3>
146+
if (userInfo.role === "educator"){
147+
moduleList.push(
148+
<div>
149+
<div className="flex justify-between flex-wrap bg-slate-400 p-4 rounded shadow" >
150+
<h3 className="text-xl font-semibold" >
151+
{module.title}
152+
</h3>
153+
<Modal
154+
title={"Add New Content"}
155+
trigger={
156+
<div className="bg-blue-500 p-2 rounded shadow hover:bg-blue-700 m-auto text-center text-sm text-white font-semibold hover:cursor-pointer" >
157+
<div>+ Add Content</div>
158+
</div>
159+
}
160+
inputFields={{
161+
title : "Content Name",
162+
}}
163+
changeHandler={handleContentChange}
164+
confirmHandler={handleCreateContent(module.ID)}
165+
fileUploadHandler={handleContentUpload}
166+
/>
167+
</div>
168+
<div>
169+
{contentList}
170+
</div>
109171
</div>
172+
);
173+
} else {
174+
moduleList.push(
110175
<div>
111-
{contentList}
176+
<div className="bg-slate-400 p-4 rounded shadow" >
177+
<h3 className="text-xl font-semibold" >{module.title}</h3>
178+
</div>
179+
<div>
180+
{contentList}
181+
</div>
112182
</div>
113-
</div>
114-
);
183+
);
184+
}
115185
});
116186

117187
let createButton = null;
@@ -126,7 +196,7 @@ export function Course() {
126196
inputFields={{
127197
title : "Module Name",
128198
}}
129-
changeHandler={handleChange}
199+
changeHandler={handleModuleChange}
130200
confirmHandler={handleCreateModule}
131201
/>
132202
}

0 commit comments

Comments
 (0)