Skip to content

Commit cbc7321

Browse files
authored
Merge branch 'sandbox-science:main' into dashboard
2 parents 7f0bb02 + a591c83 commit cbc7321

File tree

3 files changed

+126
-91
lines changed

3 files changed

+126
-91
lines changed

frontend/src/components/Modal.js

Lines changed: 36 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,6 @@
1-
import React, {useState} from 'react';
2-
3-
/**
4-
* Modal component
5-
* @param {string} title - A string which is the title at the top of the modal.
6-
* @param {React.JSX.Element} trigger - React.JSX.Element that when clicked will open the modal.
7-
* @param {Map} inputFields - {var : "Input Var Here", ...} A map of variables where things typed into a text box will be stored to the string that the user will see above that text box.
8-
* @param {(e: any) => void} changeHandler - The handler that handles changes in text and assigns them to a variable.
9-
* @param {(e: any) => Promise<void>} confirmHandler - The handler that calls an API upon clicking confirm
10-
*/
11-
export const Modal = ({title, trigger, inputFields, changeHandler, confirmHandler}) => {
1+
import React, { useState } from 'react';
122

3+
export const Modal = ({ title, trigger, inputFields, changeHandler, confirmHandler }) => {
134
const [showModal, setShowModal] = useState(false);
145

156
const close = () => {
@@ -20,42 +11,49 @@ export const Modal = ({title, trigger, inputFields, changeHandler, confirmHandle
2011
setShowModal(true);
2112
};
2213

23-
var fieldList = []
24-
Object.entries(inputFields).map(([name, text]) => (
25-
fieldList.push(
26-
<div className="p-2">
27-
<label>{text+ ": "} </label>
28-
<input className="border-2 border-slate-300 rounded p-2 size-11/12"
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+
<input
18+
className="border-2 border-gray-300 rounded p-2 size-11/12"
2919
type="text"
3020
name={name}
3121
onChange={changeHandler}
32-
/>
33-
</div>
34-
)
22+
/>
23+
</div>
3524
));
3625

3726
return (
3827
<div>
3928
<div onClick={open}>{trigger}</div>
40-
{showModal ?
41-
<div>
42-
<div className='opacity-50 bg-black fixed top-0 left-0 h-full w-full' onClick={close}/>
43-
<div className="flex-auto bg-white rounded-lg border-2 border-slate-300 absolute min-w-80 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 p-3">
44-
<h3 className="font-semibold">
45-
{title}
46-
</h3>
47-
{fieldList}
48-
<div className="grid grid-rows-1 grid-cols-2 p-2">
49-
<button className="justify-self-start border-2 border-slate-300 p-1 rounded" onClick=
50-
{() => close()}>
29+
{/* Modal */}
30+
{showModal && (
31+
<div>
32+
<div
33+
className="fixed z-[1040] overflow-auto backdrop-blur bg-opacity-50 bg-black fixed top-0 left-0 h-full w-full"
34+
onClick={close}
35+
/>
36+
{/* Modal Content */}
37+
<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">
38+
<h3 className="font-semibold text-lg mb-4">{title}</h3>
39+
{fieldList}
40+
<div className="mt-4 flex justify-end space-x-2">
41+
<button
42+
className="px-4 py-2 border border-gray-300 rounded bg-gray-200 hover:bg-gray-300"
43+
onClick={close}
44+
>
5145
Cancel
52-
</button>
53-
<button className="justify-self-end border-2 border-slate-300 p-1 rounded bg-green-400" onClick={confirmHandler}>
54-
Confirm
55-
</button>
56-
</div>
46+
</button>
47+
<button
48+
className="px-4 py-2 border border-gray-300 rounded bg-green-500 text-white hover:bg-green-600"
49+
onClick={confirmHandler}
50+
>
51+
Confirm
52+
</button>
53+
</div>
54+
</div>
5755
</div>
58-
</div>:null}
56+
)}
5957
</div>
6058
);
61-
}
59+
};

frontend/src/components/pages/CourseDashboard.js

Lines changed: 69 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import React, { useEffect, useState } from 'react';
22
import Cookies from 'js-cookie';
33
import Notiflix from 'notiflix';
4-
import {Modal} from '../Modal.js';
4+
import { Modal } from '../Modal.js';
5+
import SearchBar from '../search-bar.js'
56

67
export function CourseDashboard() {
78
const [courseInfo, setCourseInfo] = useState(null);
@@ -10,17 +11,22 @@ export function CourseDashboard() {
1011
title: "",
1112
description: "",
1213
});
13-
const [error, setError] = useState(null);
14+
const [searchQuery, setSearchQuery] = useState("");
15+
const [error, setError] = useState(null);
1416

1517
const handleChange = (e) => {
1618
const { name, value } = e.target;
1719
setNewCourse({ ...newCourse, [name]: value });
1820
};
1921

20-
const handleCreateCourse = async (e) =>{
21-
if (newCourse.title === "" || newCourse.description === ""){
22-
return
23-
}
22+
const handleSearchChange = (e) => {
23+
setSearchQuery(e.target.value);
24+
};
25+
26+
const handleCreateCourse = async () => {
27+
if (newCourse.title === "" || newCourse.description === "") {
28+
return;
29+
}
2430

2531
const userId = Cookies.get('userId');
2632
try {
@@ -31,7 +37,7 @@ export function CourseDashboard() {
3137
},
3238
body: JSON.stringify({
3339
title: newCourse.title,
34-
description: newCourse.description
40+
description: newCourse.description,
3541
}),
3642
});
3743

@@ -59,26 +65,26 @@ export function CourseDashboard() {
5965

6066
async function fetchCourses() {
6167
await fetch(`http://localhost:4000/courses/${userId}`)
62-
.then((response) => {
63-
if (!response.ok) {
64-
throw new Error('Network response was not ok');
65-
}
66-
return response.json();
67-
})
68-
.then((data) => setCourseInfo(data.courses))
69-
.catch((error) => setError(error.message));
68+
.then((response) => {
69+
if (!response.ok) {
70+
throw new Error('Network response was not ok');
71+
}
72+
return response.json();
73+
})
74+
.then((data) => setCourseInfo(data.courses))
75+
.catch((error) => setError(error.message));
7076
}
7177

7278
async function fetchUser() {
7379
await fetch(`http://localhost:4000/user/${userId}`)
74-
.then((response) => {
75-
if (!response.ok) {
76-
throw new Error('Network response was not ok');
77-
}
78-
return response.json();
79-
})
80-
.then((data) => setUserInfo(data.user))
81-
.catch((error) => setError(error.message));
80+
.then((response) => {
81+
if (!response.ok) {
82+
throw new Error('Network response was not ok');
83+
}
84+
return response.json();
85+
})
86+
.then((data) => setUserInfo(data.user))
87+
.catch((error) => setError(error.message));
8288
}
8389

8490
fetchCourses();
@@ -89,41 +95,51 @@ export function CourseDashboard() {
8995
if (!courseInfo || !userInfo) return <p>Loading...</p>;
9096

9197
let createButton = null;
92-
if (userInfo.role === "educator"){
93-
createButton = <Modal
94-
title={"Create New Course"}
95-
trigger={
96-
<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" >
97-
<div>+ New Course</div>
98-
</div>
99-
}
100-
inputFields={{
101-
title : "Course Name",
102-
description : "Course Description"
103-
}}
104-
changeHandler={handleChange}
105-
confirmHandler={handleCreateCourse}
106-
/>
98+
if (userInfo.role === "educator") {
99+
createButton = (
100+
<Modal
101+
title={"Create New Course"}
102+
trigger={
103+
<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">
104+
<div>+ New Course</div>
105+
</div>
106+
}
107+
inputFields={{
108+
title: "Course Name",
109+
description: "Course Description",
110+
}}
111+
changeHandler={handleChange}
112+
confirmHandler={async () => {
113+
await handleCreateCourse();
114+
}}
115+
/>
116+
117+
);
107118
}
108119

109-
var courseList = [];
110-
//Push all courses into courseList
111-
courseInfo.forEach(course => {
112-
courseList.push(
113-
<a href={`/courses/${course.ID}`}>
114-
<div className="bg-gray-100 p-4 rounded shadow hover:bg-gray-300" >
115-
<h3 className="text-xl font-semibold truncate overflow-hidden" >{course.title}</h3>
116-
<p className="mt-2">{course.description}</p>
117-
</div>
118-
</a>
119-
)
120-
});
121-
120+
const filteredCourses = courseInfo.filter((course) =>
121+
course.title.toLowerCase().includes(searchQuery.toLowerCase())
122+
);
123+
124+
const courseList = filteredCourses.map((course) => (
125+
<a href={`/courses/${course.ID}`} key={course.ID}>
126+
<div className="bg-gray-100 p-4 rounded shadow hover:bg-gray-300">
127+
<h3 className="text-xl font-semibold truncate overflow-hidden">{course.title}</h3>
128+
<p className="mt-2">{course.description}</p>
129+
</div>
130+
</a>
131+
));
132+
122133
return (
123134
<div className="p-6">
124-
<div className="flex justify-between flex-wrap mb-5">
135+
<div className="mb-5">
125136
<h1 className="text-2xl font-bold">Courses</h1>
126-
{createButton}
137+
</div>
138+
<div className="mb-5">
139+
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4">
140+
{createButton}
141+
<SearchBar onChange={handleSearchChange} />
142+
</div>
127143
</div>
128144
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
129145
{courseList}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
2+
3+
export default function SearchBar({ onChange }) {
4+
return (
5+
<div className="grow">
6+
<label htmlFor="default-search" className="pt-2 text-sm font-medium sr-only">Search</label>
7+
<div className="relative">
8+
<div className="absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none">
9+
<MagnifyingGlassIcon className='w-4 h-4 text-gray-500' />
10+
</div>
11+
<input
12+
onChange={onChange}
13+
type="search"
14+
id="default-search"
15+
className="input w-full max-w-md pl-8 h-12 rounded-lg border"
16+
placeholder="Search for a course..."
17+
/>
18+
</div>
19+
</div>
20+
);
21+
}

0 commit comments

Comments
 (0)