Skip to content

Commit ccb500e

Browse files
authored
Merge pull request #20 from zachspang/dashboard
[Feature] Course thumbnails
2 parents ffbfd04 + 9d2a35e commit ccb500e

File tree

10 files changed

+176
-46
lines changed

10 files changed

+176
-46
lines changed

backend/internal/handlers/courses.go

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -368,10 +368,12 @@ func EditContent(c *fiber.Ctx) error {
368368
path := fmt.Sprintf("/%d/%s", module.Course.ID, strconv.FormatUint(uint64(content.ID), 10)+fileExtension)
369369

370370
//Remove previous attachment if there is one
371-
if _, err := os.Stat("./content" + path); os.IsExist(err) {
371+
if _, err := os.Stat("./content" + path); err == nil {
372372
if err := os.Remove("./content" + path); err != nil {
373373
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
374374
}
375+
} else if !os.IsNotExist(err){
376+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
375377
}
376378

377379
if err := c.SaveFile(file, "./content"+path); err != nil {
@@ -420,10 +422,12 @@ func DeleteFile(c *fiber.Ctx) error {
420422
}
421423

422424
//Delete file
423-
if _, err := os.Stat("./content" + content.Path); os.IsExist(err) {
425+
if _, err := os.Stat("./content" + content.Path); err == nil {
424426
if err := os.Remove("./content" + content.Path); err != nil {
425427
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
426428
}
429+
} else {
430+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
427431
}
428432

429433
//Update database entry
@@ -435,3 +439,57 @@ func DeleteFile(c *fiber.Ctx) error {
435439
"message": "Successfully deleted file",
436440
})
437441
}
442+
443+
// Edit thumbnail of course
444+
func EditThumbnail(c *fiber.Ctx) error {
445+
creator_id, err := strconv.Atoi(c.Params("creator_id"))
446+
if err != nil {
447+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid creator_id"})
448+
}
449+
450+
course_id, err := strconv.Atoi(c.Params("course_id"))
451+
if err != nil {
452+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid course_id"})
453+
}
454+
455+
var course entity.Course
456+
// Check if the course exists
457+
if err := database.DB.Where("id = ?", course_id).First(&course).Error; err != nil {
458+
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "Course not found"})
459+
}
460+
461+
//Verify that the account attempting to create a module is the creator of the course
462+
if course.CreatorID != creator_id {
463+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "User is not the course creator"})
464+
}
465+
466+
file, err := c.FormFile("file")
467+
if err != nil {
468+
if err.Error() != "there is no uploaded file associated with the given key" {
469+
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid file"})
470+
}
471+
}
472+
473+
if file != nil {
474+
if err := os.MkdirAll(fmt.Sprintf("./content/%d/", course_id), 0777); err != nil {
475+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
476+
}
477+
478+
//Remove previous attachment if there is one
479+
if _, err := os.Stat(fmt.Sprintf("./content/%d/thumbnail.png", course_id)); err == nil {
480+
if err := os.Remove(fmt.Sprintf("./content/%d/thumbnail.png", course_id)); err != nil {
481+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
482+
}
483+
} else if !os.IsNotExist(err){
484+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
485+
}
486+
487+
if err := c.SaveFile(file, fmt.Sprintf("./content/%d/thumbnail.png", course_id)); err != nil {
488+
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"message": err.Error()})
489+
}
490+
}
491+
492+
return c.JSON(fiber.Map{
493+
"message": "Successfully updated thumbnail",
494+
})
495+
}

backend/internal/router/route.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,5 +90,6 @@ func SetupRoutes(app *fiber.App) {
9090
app.Post("/create-content/:creator_id/:module_id", handlers.CreateContent)
9191
app.Post("/delete-file/:creator_id/:content_id", handlers.DeleteFile)
9292
app.Post("/edit-content/:creator_id/:content_id", handlers.EditContent)
93+
app.Post("/edit-thumbnail/:creator_id/:course_id", handlers.EditThumbnail)
9394
app.Post("/enroll/:user_id/:course_id", handlers.Enroll)
9495
}
165 Bytes
Loading
165 Bytes
Loading
165 Bytes
Loading
165 Bytes
Loading
165 Bytes
Loading

frontend/src/components/Modal.js

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,29 @@ export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandl
1111
setShowModal(true);
1212
};
1313

14-
const fieldList = Object.entries(inputFields).map(([name, text]) => (
15-
<div className="p-2" key={name}>
16-
<label className="block font-medium">{text + ":"}</label>
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-
31-
</div>
32-
));
33-
14+
var fieldList = null
15+
if (inputFields != null) {
16+
fieldList = Object.entries(inputFields).map(([name, text]) => (
17+
<div className="p-2" key={name}>
18+
<label className="block font-medium">{text + ":"}</label>
19+
{name === "body" ?
20+
<textarea
21+
className="border-2 border-gray-300 rounded p-2 size-11/12"
22+
type="text"
23+
name={name}
24+
onChange={changeHandler}
25+
/>:
26+
<input
27+
className="border-2 border-gray-300 rounded p-2 size-11/12"
28+
type="text"
29+
name={name}
30+
onChange={changeHandler}
31+
/>}
32+
33+
</div>
34+
));
35+
}
36+
3437
return (
3538
<div>
3639
<div onClick={open}>{trigger}</div>

frontend/src/components/pages/Course.js

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ 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);
1213
const [newContentName, setNewContentName] = useState({
1314
title: "",
1415
});
@@ -85,6 +86,38 @@ export function Course() {
8586
}
8687
};
8788

89+
const handleThumbnailUpload = (e) => {
90+
setFile(e.target.files[0]);
91+
}
92+
93+
const handleEditThumbnail = (courseID) => async (e) => {
94+
if (file === null){
95+
return
96+
}
97+
98+
const userId = Cookies.get('userId');
99+
try {
100+
const formData = new FormData();
101+
formData.append("file", file);
102+
const response = await fetch(`http://localhost:4000/edit-thumbnail/${userId}/${courseID}`, {
103+
method: "POST",
104+
body: formData,
105+
});
106+
107+
const data = await response.json();
108+
if (response.ok) {
109+
Notiflix.Notify.success("Thumbnail upload successful!");
110+
setTimeout(() => {
111+
window.location.reload();
112+
}, 500);
113+
} else {
114+
Notiflix.Notify.failure(data.message || "Thumbnail upload failed");
115+
}
116+
} catch (error) {
117+
Notiflix.Notify.failure("Error occurred during thumbnail upload");
118+
}
119+
};
120+
88121
useEffect(() => {
89122
const userId = Cookies.get('userId');
90123

@@ -194,11 +227,28 @@ export function Course() {
194227
/>
195228
}
196229

230+
let editThumbnail = null;
231+
if (userInfo.role === "educator"){
232+
editThumbnail = <Modal
233+
title={"Edit Thumbnail"}
234+
trigger={
235+
<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" >
236+
<div>Edit Thumbnail</div>
237+
</div>
238+
}
239+
confirmHandler={handleEditThumbnail(courseID)}
240+
fileUploadHandler={handleThumbnailUpload}
241+
/>
242+
}
243+
197244
return (
198245
<div className="p-6">
199246
<div className="flex justify-between flex-wrap mb-5">
200247
<h1 className="text-2xl font-bold mb-4">{courseInfo.title}</h1>
201-
{createButton}
248+
<div className="flex flex-1 justify-end flex-wrap gap-5 ml-20">
249+
{editThumbnail}
250+
{createButton}
251+
</div>
202252
</div>
203253
<div className="grid grid-cols-1 gap-6">
204254
{moduleList}

frontend/src/components/pages/CourseDashboard.js

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -139,21 +139,41 @@ export function CourseDashboard() {
139139
);
140140

141141
const myCourses = courseInfo.map((course) => (
142-
<a href={`/courses/${course.ID}`} key={course.ID}>
143-
<div className="bg-gray-100 p-4 rounded shadow hover:bg-gray-300">
144-
<h3 className="text-xl font-semibold truncate overflow-hidden">{course.title}</h3>
145-
<p className="mt-2">{course.description}</p>
142+
<a href={`/courses/${course.ID}`} key={course.ID} className="max-w-[40vw] aspect-[9/8]">
143+
<div className="bg-gray-100 rounded shadow hover:bg-gray-300 flex flex-col h-full w-full outline outline-1 outline-black/25">
144+
<div className="h-[60%] ">
145+
<img
146+
className="rounded outline outline-1 outline-black/40 object-fill w-full h-full block aspect-[16/9]"
147+
src={process.env.PUBLIC_URL + `/content/${course.ID}/thumbnail.png`}
148+
onError={(e)=>{e.target.onError = null; e.target.src = `/default_thumbnails/tn${course.ID % 5}.png`}}
149+
alt="Thumbnail"
150+
/>
151+
</div>
152+
<div className="h-[40%] p-2 md:p-4 xl:p-2 overflow-hidden">
153+
<h3 className="text-base font-semibold truncate overflow-hidden">{course.title}</h3>
154+
<p className="truncate overflow-hidden">{course.description}</p>
155+
</div>
146156
</div>
147157
</a>
148158
));
149159

150160
const allCourses = filteredCourses.map((course) => (
151-
<div key={course.ID} className="course-box">
152-
<a href={`/courses/${course.ID}`} key={course.ID}>
153-
<h3><b>{course.title}</b></h3>
154-
<p>{course.description}</p>
155-
</a>
156-
</div>
161+
<a href={`/courses/${course.ID}`} key={course.ID} className="max-w-[40vw] aspect-[9/8]">
162+
<div className="bg-gray-100 rounded shadow hover:bg-gray-300 flex flex-col h-full w-full outline outline-1 outline-black/25">
163+
<div className="h-[60%] ">
164+
<img
165+
className="rounded outline outline-1 outline-black/40 object-fill w-full h-full block aspect-[16/9]"
166+
src={process.env.PUBLIC_URL + `/content/${course.ID}/thumbnail.png`}
167+
onError={(e)=>{e.target.onError = null; e.target.src = `/default_thumbnails/tn${course.ID % 5}.png`}}
168+
alt="Thumbnail"
169+
/>
170+
</div>
171+
<div className="h-[40%] pl-2 p-1 overflow-hidden">
172+
<h3 className="text-base font-semibold truncate overflow-hidden">{course.title}</h3>
173+
<p className="truncate overflow-hidden">{course.description}</p>
174+
</div>
175+
</div>
176+
</a>
157177
))
158178

159179
return (
@@ -171,26 +191,24 @@ export function CourseDashboard() {
171191
{/* My Courses Section */}
172192
<div className="mt-10">
173193
<h2 className="text-xl font-semibold mb-4">My Courses</h2>
174-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
194+
<div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-9">
175195
{myCourses}
176196
</div>
177197
</div>
178198

179199
{/* All Courses Section */}
180200
<div className="mt-10">
181201
<h2 className="text-xl font-semibold mb-4">All Courses</h2>
182-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
183-
<div className="course-list">
184-
{loading ? (
185-
<p>Loading courses...</p>
186-
) : filteredCourses.length > 0 ? (
187-
allCourses
188-
) : (
189-
<div className="course-box">
190-
<p>Nothing to show, yet</p>
191-
</div>
192-
)}
193-
</div>
202+
<div className="grid grid-cols-2 sm:grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-8 gap-7">
203+
{loading ? (
204+
<p>Loading courses...</p>
205+
) : filteredCourses.length > 0 ? (
206+
allCourses
207+
) : (
208+
<div className="course-box">
209+
<p>Nothing to show, yet</p>
210+
</div>
211+
)}
194212
</div>
195213
</div>
196214
</div>

0 commit comments

Comments
 (0)