Skip to content

Commit 065eec7

Browse files
committed
Refactor adding content and add edit content button
1 parent 8d929a8 commit 065eec7

File tree

7 files changed

+194
-46
lines changed

7 files changed

+194
-46
lines changed

backend/internal/entity/content.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ type Content struct {
66
gorm.Model
77
Title string `gorm:"size:255;not null;" json:"title"`
88
Path string `gorm:"size:255;not null;" json:"path"`
9+
Body string `gorm:"size:2000;" json:"body"`
910
ModuleID uint
1011
Module Module `gorm:"foreignKey:ModuleID;references:ID" json:"-"`
1112
}

backend/internal/handlers/courses.go

Lines changed: 71 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ func CreateModule(c *fiber.Ctx) error {
218218

219219
//Verify that the account attempting to create a module is the creator of the course
220220
if course.CreatorID != creator_id {
221-
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User is not the course creator"})
221+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User is not the course creator"})
222222
}
223223

224224
// Create module
@@ -263,7 +263,7 @@ func CreateContent(c *fiber.Ctx) error {
263263

264264
//Verify that the account attempting to create content is the creator of the course
265265
if module.Course.CreatorID != creator_id {
266-
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "User is not the course creator"})
266+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User is not the course creator"})
267267
}
268268

269269
// Create content database entry
@@ -277,30 +277,84 @@ func CreateContent(c *fiber.Ctx) error {
277277
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
278278
}
279279

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"})
280+
return c.JSON(fiber.Map{
281+
"message": "Content created successfully",
282+
"content": content,
283+
})
284+
}
285+
286+
// Edit content inside a module
287+
func EditContent(c *fiber.Ctx) error {
288+
creator_id, err := strconv.Atoi(c.Params("creator_id"))
289+
if err != nil {
290+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid creator_id"})
284291
}
285292

286-
var fileExtension = file.Filename[strings.LastIndex(file.Filename, "."):]
293+
content_id, err := strconv.Atoi(c.Params("content_id"))
294+
if err != nil {
295+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid content_id"})
296+
}
287297

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()})
298+
var content entity.Content
299+
// Check if the content exists
300+
if err := database.DB.Model(entity.Content{}).Preload("Module.Course").Where("id = ?", content_id).First(&content).Error; err != nil {
301+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Course not found"})
302+
}
303+
304+
module := content.Module
305+
//Verify that the account attempting to edit is the creator of the course
306+
if module.Course.CreatorID != creator_id {
307+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User is not the course creator"})
290308
}
291309

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()})
310+
title := c.FormValue("title")
311+
if title != "" {
312+
if err := database.DB.Model(&content).Update("title", title).Error; err != nil{
313+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
314+
}
315+
}
316+
317+
body := c.FormValue("body")
318+
if body != "" {
319+
if err := database.DB.Model(&content).Update("body", body).Error; err != nil{
320+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
321+
}
295322
}
296323

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()})
324+
file, err := c.FormFile("file");
325+
if err != nil {
326+
if err.Error() != "there is no uploaded file associated with the given key"{
327+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid file"})
328+
}
329+
}
330+
331+
if file != nil {
332+
var fileExtension = file.Filename[strings.LastIndex(file.Filename, "."):]
333+
334+
if err := os.MkdirAll(fmt.Sprintf("./content/%d/", module.Course.ID), 0777); err != nil{
335+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
336+
}
337+
338+
path := fmt.Sprintf("/%d/%s", module.Course.ID, strconv.FormatUint(uint64(content.ID), 10) + fileExtension)
339+
340+
//Remove previous attachment if there is one
341+
if _, err := os.Stat("./content"+path); os.IsExist(err){
342+
if err := os.Remove("./content"+path); err != nil{
343+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
344+
}
345+
}
346+
347+
if err := c.SaveFile(file, "./content"+path); err != nil{
348+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
349+
}
350+
351+
// Add the path
352+
if err := database.DB.Model(&content).Update("path", path).Error; err != nil{
353+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
354+
}
300355
}
301356

302357
return c.JSON(fiber.Map{
303-
"message": "Content created successfully",
304-
"content": content,
358+
"message": "Successfully updated content",
305359
})
306360
}

backend/internal/router/route.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,6 @@ func SetupRoutes(app *fiber.App) {
8787
app.Post("/create-course/:creator_id", handlers.CreateCourse)
8888
app.Post("/create-module/:creator_id/:course_id", handlers.CreateModule)
8989
app.Post("/create-content/:creator_id/:module_id", handlers.CreateContent)
90+
app.Post("/edit-content/:creator_id/:content_id", handlers.EditContent)
9091
app.Post("/enroll/:user_id/:course_id", handlers.Enroll)
9192
}

docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ services:
5555
- WATCHPACK_POLLING=true
5656
volumes:
5757
- './frontend:/app'
58-
- ./content:/app/src/content
58+
- ./content:/app/public/content
5959

6060
volumes:
6161
learning-platform-data:

frontend/src/components/Modal.js

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,20 @@ export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandl
1414
const fieldList = Object.entries(inputFields).map(([name, text]) => (
1515
<div className="p-2" key={name}>
1616
<label className="block font-medium">{text + ":"}</label>
17-
<input
18-
className="border-2 border-gray-300 rounded p-2 size-11/12"
19-
type="text"
20-
name={name}
21-
onChange={changeHandler}
22-
/>
17+
{name === "body" ?
18+
<textarea
19+
className="border-2 border-gray-300 rounded p-2 size-11/12"
20+
type="text"
21+
name={name}
22+
onChange={changeHandler}
23+
/>:
24+
<input
25+
className="border-2 border-gray-300 rounded p-2 size-11/12"
26+
type="text"
27+
name={name}
28+
onChange={changeHandler}
29+
/>}
30+
2331
</div>
2432
));
2533

frontend/src/components/pages/Content.js

Lines changed: 105 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,69 @@
11
import React, { useEffect, useState } from 'react';
22
import { useParams } from 'react-router-dom';
3+
import Cookies from 'js-cookie';
4+
import Notiflix from 'notiflix';
5+
import {Modal} from '../Modal.js';
36

47
export function Content() {
58
const { courseID } = useParams();
69
const { contentID } = useParams();
710
const [courseInfo, setCourseInfo] = useState(null);
11+
const [userInfo, setUserInfo] = useState(null);
812
const [contentInfo, setContentInfo] = useState(null);
913
const [error, setError] = useState(null);
10-
14+
const [file, setFile] = useState(null);
15+
const [newContentInfo, setNewContentInfo] = useState({
16+
title: "",
17+
body: "",
18+
});
19+
20+
const handleContentChange = (e) => {
21+
const { name, value } = e.target;
22+
setNewContentInfo({...newContentInfo, [name]: value });
23+
};
24+
25+
const handleContentUpload = (e) => {
26+
setFile(e.target.files[0]);
27+
}
28+
29+
const handleEditContent = (contentID) => async (e) =>{
30+
if (newContentInfo.title === "" && newContentInfo.body === "" && file === null){
31+
return
32+
}
33+
34+
const userId = Cookies.get('userId');
35+
try {
36+
const formData = new FormData();
37+
formData.append("file", file);
38+
formData.append("title", newContentInfo.title);
39+
formData.append("body", newContentInfo.body)
40+
const response = await fetch(`http://localhost:4000/edit-content/${userId}/${contentID}`, {
41+
method: "POST",
42+
body: formData,
43+
});
44+
45+
const data = await response.json();
46+
if (response.ok) {
47+
Notiflix.Notify.success("Content creation successful!");
48+
setTimeout(() => {
49+
window.location.reload();
50+
}, 500);
51+
} else {
52+
Notiflix.Notify.failure(data.message || "Content creation failed");
53+
}
54+
} catch (error) {
55+
Notiflix.Notify.failure("Error occurred during content creation");
56+
}
57+
};
58+
1159
useEffect(() => {
60+
const userId = Cookies.get('userId');
61+
62+
if (!userId) {
63+
setError('User ID not found');
64+
return;
65+
}
66+
1267
async function fetchCourse() {
1368
fetch(`http://localhost:4000/course/${courseID}`)
1469
.then((response) => {
@@ -33,28 +88,65 @@ export function Content() {
3388
.catch((error) => setError(error.message));
3489
}
3590

91+
async function fetchUser() {
92+
await fetch(`http://localhost:4000/user/${userId}`)
93+
.then((response) => {
94+
if (!response.ok) {
95+
throw new Error('Network response was not ok');
96+
}
97+
return response.json();
98+
})
99+
.then((data) => setUserInfo(data.user))
100+
.catch((error) => setError(error.message));
101+
}
102+
36103
fetchCourse();
37104
fetchContent();
105+
fetchUser();
38106
}, [courseID, contentID]);
39107

40108
if (error) return <p>Error: {error}</p>;
41-
if (!courseInfo) return <p>Loading...</p>;
42-
43-
const images = require.context("../../content", true);
109+
if (!courseInfo || !contentInfo || !userInfo) return <p>Loading...</p>;
44110

111+
let editButton = null;
112+
if (userInfo.role === "educator") {
113+
editButton = (
114+
<Modal
115+
title={"Edit Content"}
116+
trigger={
117+
<div className="bg-blue-500 p-3 rounded shadow hover:bg-blue-700 m-auto text-center text-sm text-white font-semibold hover:cursor-pointer">
118+
<div>Edit Content</div>
119+
</div>
120+
}
121+
inputFields={{
122+
title: "Content Name",
123+
body: "Body",
124+
}}
125+
changeHandler={handleContentChange}
126+
confirmHandler={handleEditContent(contentID)}
127+
fileUploadHandler={handleContentUpload}
128+
/>
129+
130+
);
131+
}
132+
45133
return (
46134
<div className="p-6">
47-
<a href={`/courses/${courseID}`}>
48-
<span className="text-2xl font-bold mb-4 hover:underline ">{courseInfo.title}</span>
49-
</a>
50-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
51-
Course id: {courseID}
135+
<div className="flex justify-between">
136+
<a href={`/courses/${courseID}`}>
137+
<span className="text-2xl font-bold mb-4 hover:underline ">{courseInfo.title}</span>
138+
</a>
139+
{editButton}
140+
</div>
141+
<div className="text-xl font-bold">
142+
{contentInfo.title}
52143
</div>
53-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
54-
Content id: {contentID}
144+
<div>
145+
{contentInfo.body}
55146
</div>
56-
<img src={images("." + contentInfo.path)} alt=""/>
57-
147+
{contentInfo.path !== "" ?
148+
<img src={process.env.PUBLIC_URL + "/content" + contentInfo.path} alt=""/> : null
149+
}
58150
</div>
59151

60152
);

frontend/src/components/pages/Course.js

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ 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);
1312
const [newContentName, setNewContentName] = useState({
1413
title: "",
1514
});
@@ -58,19 +57,14 @@ export function Course() {
5857
setNewContentName({[name]: value });
5958
};
6059

61-
const handleContentUpload = (e) => {
62-
setFile(e.target.files[0]);
63-
}
64-
6560
const handleCreateContent = (moduleID) => async (e) =>{
66-
if (newContentName.title === "" || file === null){
61+
if (newContentName.title === ""){
6762
return
6863
}
6964

7065
const userId = Cookies.get('userId');
7166
try {
7267
const formData = new FormData();
73-
formData.append("file", file);
7468
formData.append("title", newContentName.title);
7569
const response = await fetch(`http://localhost:4000/create-content/${userId}/${moduleID}`, {
7670
method: "POST",
@@ -162,7 +156,6 @@ export function Course() {
162156
}}
163157
changeHandler={handleContentChange}
164158
confirmHandler={handleCreateContent(module.ID)}
165-
fileUploadHandler={handleContentUpload}
166159
/>
167160
</div>
168161
<div>
@@ -207,7 +200,6 @@ export function Course() {
207200
<h1 className="text-2xl font-bold mb-4">{courseInfo.title}</h1>
208201
{createButton}
209202
</div>
210-
211203
<div className="grid grid-cols-1 gap-6">
212204
{moduleList}
213205
</div>

0 commit comments

Comments
 (0)