Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions frontend/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://upload-widget.cloudinary.com/latest/global/all.js" type="text/javascript"></script>
<title>Vite + React</title>
<link rel="stylesheet" href="src/tailwind.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>

</body>
</html>
16 changes: 9 additions & 7 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.0",
"react-router-dom": "^7.6.2",
"react-dropzone": "^14.3.8",
"cloudinary": "^2.6.1",
"react-icons": "^5.5.0",
Expand Down
Binary file added frontend/src/assets/Star 0.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/Star 1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions frontend/src/assets/back_arrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/download_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/notes_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/share_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added frontend/src/assets/texture.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
119 changes: 89 additions & 30 deletions frontend/src/components/DiscussionList.jsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,100 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';

export default function DiscussionList({ subjectId }) {
export default function DiscussionList({ subjectId ,searchQuery=''}) {
const [list, setList] = useState([]);
const [txt, setTxt] = useState('');

const load = ()=>fetch(`http://localhost:4000/subjects/${subjectId}/discussions`)
.then(r=>r.json())
.then(res => setList(res.data));

useEffect(load, [subjectId]);

const post = async e => {
e.preventDefault();
await fetch(`http://localhost:4000/subjects/${subjectId}/discussions`, {
method:'POST',
headers:{ 'Content-Type':'application/json', 'x-user-id':1 },
body: JSON.stringify({ content: txt })
});
setTxt('');

const load = useCallback(async()=> {
try {
fetch(`http://localhost:4000/subjects/${subjectId}/discussions`)
.then((r) => r.json())
.then((res) => setList(res.data));
} catch (err) {
console.error('Error loading discussions:', err);
}
}, [subjectId]);

useEffect(() => {
load();
return () => {
try {
// future cleanup (if needed)
} catch (err) {
console.error('Cleanup error in DiscussionList:', err);
}
};
}, [load]);

const post = async (e) => {
e.preventDefault();

try {
await fetch(`http://localhost:4000/subjects/${subjectId}/discussions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-user-id': 1,
},
body: JSON.stringify({ content: txt }),
});
setTxt('');

await load();

} catch (err) {
console.error('Error posting discussion:', err);
}
};

return (
<div>
<h2>Discussion</h2>
<form onSubmit={post}>
<textarea value={txt} onChange={e=>setTxt(e.target.value)} required/>
<button type="submit">Post</button>
<div className="min-w-[60vw] p-6 space-y-6 bg-white/10 rounded-xl shadow-lg text-white">
{/* Heading */}
<h2 className="text-2xl font-bold text-center">💬 Discussion Forum</h2>

{/* Form */}
<form id='discussion' onSubmit={post} className="flex flex-col gap-4 bg-white/50 p-4 rounded-xl">
<textarea
value={txt}
onChange={(e) => setTxt(e.target.value)}
required
placeholder="Share your thoughts..."
className="w-full min-h-[100px] p-3 rounded-lg text-black resize-y border border-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
<button
type="submit"
className="self-start px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition"
>
➕ Post
</button>
</form>
<ul>
{list.map(d=>(
<li key={d.id}>
<p>{d.content}</p>
<small>— {d.user.name} at {new Date(d.createdAt).toLocaleString()}</small>
</li>
))}
</ul>

{/* Discussion List */}
<ul className="space-y-4">
{list.filter((d) =>
!searchQuery || // if searchQuery is empty, show all
d.content.toString().toLowerCase().includes(searchQuery.toString().toLowerCase()) ||
d.user?.name?.toString().toLowerCase().includes(searchQuery.toString().toLowerCase())
).length === 0 ? (
<p className="text-gray-300 text-center italic">No discussions yet. Be the first to post!</p>
) : (
list
.filter((d) =>
!searchQuery ||
d.content.toString().toLowerCase().includes(searchQuery.toString().toLowerCase()) ||
d.user?.name?.toString().toLowerCase().includes(searchQuery.toString().toLowerCase())
)
.map((d) => (
<li key={d.id} className="bg-white/70 text-black p-4 rounded-lg shadow-sm">
<p className="text-base">{d.content}</p>
<small className="text-sm text-gray-600 block mt-2">
— <span className="font-medium">{d.user.name}</span>,{' '}
{new Date(d.createdAt).toLocaleString()}
</small>
</li>
))
)}
</ul>

</div>
);
}
160 changes: 119 additions & 41 deletions frontend/src/components/LabList.jsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,155 @@
import { useEffect, useState } from 'react';
import UploadModal from './Upload';
import { useEffect, useState, useCallback } from "react";
import UploadModal from "./Upload";
import share_icon from "../assets/share_icon.png";
import download_icon from "../assets/download_icon.png";
import notes_icon from "../assets/notes_icon.png";
import star_unfilled from "../assets/star 0.png";

export default function LabList({ subjectId }) {
export default function LabList({ subjectId, searchQuery = "" }) {
const [labs, setLabs] = useState([]);
const [title, setTitle] = useState('');
const [title, setTitle] = useState("");
const [modalOpen, setmodalOpen] = useState(false);

const load = ()=>fetch(`http://localhost:4000/subjects/${subjectId}/labs`)
.then(r=>r.json())
.then(res => setLabs(res.data));
const load = useCallback(async () => {
try {
fetch(`http://localhost:4000/subjects/${subjectId}/labs`)
.then((r) => r.json())
.then((res) => setLabs(res.data));
} catch (err) {
console.error("Error loading ", err);
}
}, [subjectId]);

useEffect(load, [subjectId]);
useEffect(() => {
load();
return () => {
try {
// future cleanup (if needed)
} catch (err) {
console.error("Cleanup error lablist", err);
}
};
}, [load]);

const add = async (uploadTitle, cloudinaryUrl) => {
const payload = {
title: uploadTitle,
url: cloudinaryUrl
url: cloudinaryUrl,
};

await fetch(`http://localhost:4000/subjects/${subjectId}/labs`, {
method: 'POST',
headers: {
'x-user-id': 1,
'x-user-role': 'USER',
'Content-Type': 'application/json'
method: "POST",
headers: {
"x-user-id": 1,
"x-user-role": "USER",
"Content-Type": "application/json",
},
body: JSON.stringify(payload)
body: JSON.stringify(payload),
});
setTitle('');

setTitle("");
load();
};

const del = async id => {
await fetch(`http://localhost:4000/labs/${id}`, { method:'DELETE' });
load();
const del = async (id) => {
try {
await fetch(`http://localhost:4000/labs/${id}`, { method: "DELETE" });
load();
} catch (err) {
console.error("Error in delete:", err);
}
};

const handleUploadComplete = async ({ url, public_id }) => {
await add(public_id, url);
setTitle("")
setmodalOpen(false)
}
const handleUploadComplete = async ({ url }) => {
await add(title, url);
setTitle("");
setmodalOpen(false);
};

const handleButtonClick = () => {
if (title.trim() === "") {
alert("Please enter a title before uploading.")
return
alert("Please enter a title before uploading.");
return;
}
setmodalOpen(true)
}
setmodalOpen(true);
};

return (
<div>
<h2>Lab Materials</h2>
<form onSubmit={add}>
<input value={title} onChange={e=>setTitle(e.target.value)} placeholder="Title" required/>
<button type="button" onClick={handleButtonClick} disabled={title.trim()===""}>Upload Lab</button>
<UploadModal
<div className="min-w-[80vw] mx-auto p-6 space-y-6">
<h2 className="text-2xl font-bold text-center text-white">
Lab Materials
</h2>

<form className="flex flex-col md:flex-row items-center justify-center gap-4">
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title"
required
className="w-full md:w-1/3 px-4 py-2 border border-gray-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-400"
/>
<button
type="button"
onClick={handleButtonClick}
className="px-6 py-2 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition"
>
Upload Lab
</button>
<UploadModal
open={modalOpen}
onClose={() => setmodalOpen(false)}
onComplete={handleUploadComplete}
title={title}
/>
</form>
<ul>
{labs.map(l=>(
<li key={l.id}>
<a href={l.url} target="_blank" rel="noopener noreferrer">{l.title}</a>
<span> by {l.user.name}</span>
<button onClick={()=>del(l.id)}>Delete</button>

<ul className="space-y-4">
{(searchQuery
? labs.filter(
(l) =>
l.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
l.user.name.toLowerCase().includes(searchQuery.toLowerCase()),
)
: labs
).map((l) => (
<li
key={l.id}
className="flex items-center justify-between bg-white px-4 py-3 rounded-2xl shadow-md"
>
<div>
<img src={notes_icon} alt="icon" />
</div>
<div className="flex flex-col md:flex-row md:items-center gap-2">
<a
href={`http://localhost:4000${l.url}`}
target="_blank"
rel="noopener noreferrer"
className="text-base font-semibold text-blue-700 hover:underline"
>
{l.title}
</a>
<span className="text-sm text-gray-500">by {l.user.name}</span>
</div>
<div className="flex items-center gap-3">
<img src={share_icon} alt="share" />
<a
href={`http://localhost:4000${l.url}`}
download
className="text-sm text-blue-600 hover:underline hover:text-blue-800"
>
<img src={download_icon} alt="download" />
</a>
<img src={star_unfilled} alt="favourites" />
<button
onClick={() => del(l.id)}
className="text-sm text-red-500 hover:underline hover:text-red-700"
>
Delete
</button>
</div>
</li>
))}
</ul>
</div>
);
}
}
Loading
Loading