Skip to content

Commit 5cb4030

Browse files
committed
Solve
Signed-off-by: Aditya Raut <araut7798@gmail.com>
1 parent 830077a commit 5cb4030

5 files changed

Lines changed: 174 additions & 67 deletions

File tree

client/src/components/CourseView/AssignmentCard.jsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,13 +221,17 @@ function AssignmentCard({
221221
<label className="flex items-center gap-3 px-4 py-3 bg-[var(--surface-elevated)] border border-[var(--border)] rounded-2xl cursor-pointer hover:border-[var(--accent)]/40 transition-all duration-300">
222222
<span className="text-lg">📎</span>
223223
<span className="text-sm text-[var(--muted)] flex-1 truncate">
224-
{submissionFile ? submissionFile.name : "Attach PDF or DOCX (optional)"}
224+
{submissionFile
225+
? submissionFile.name
226+
: "Attach PDF or DOCX (optional)"}
225227
</span>
226228
<input
227229
type="file"
228230
accept=".pdf,.doc,.docx"
229231
className="hidden"
230-
onChange={(e) => onSubmit(assignment.id, e.target.files[0] || null, "file")}
232+
onChange={(e) =>
233+
onSubmit(assignment.id, e.target.files[0] || null, "file")
234+
}
231235
/>
232236
</label>
233237
{submissionFile && (

client/src/components/CourseView/AssignmentModal.jsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,17 @@ function AssignmentModal({
7777
{/* Optional document attachment */}
7878
<div>
7979
<label className="block text-[11px] font-bold text-[var(--text)] uppercase tracking-wider mb-2 ml-1 opacity-80">
80-
Attach Document <span className="normal-case font-normal opacity-60">(PDF or DOCX, optional)</span>
80+
Attach Document{" "}
81+
<span className="normal-case font-normal opacity-60">
82+
(PDF or DOCX, optional)
83+
</span>
8184
</label>
8285
<label className="flex items-center gap-3 px-4 py-3 bg-[var(--surface-elevated)] border border-[var(--border)] rounded-2xl cursor-pointer hover:border-[var(--accent)]/40 transition-all duration-300">
8386
<span className="text-xl">📎</span>
8487
<span className="text-sm text-[var(--muted)] flex-1 truncate">
85-
{form.attachmentFile ? form.attachmentFile.name : "Click to choose file…"}
88+
{form.attachmentFile
89+
? form.attachmentFile.name
90+
: "Click to choose file…"}
8691
</span>
8792
<input
8893
type="file"

client/src/components/CourseView/QuizzesTab.jsx

Lines changed: 67 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { useNavigate } from "react-router-dom";
22

3-
function QuizzesTab({ quizzes, isTeacher, myQuizResults = {}, onDelete, onAddClick }) {
3+
function QuizzesTab({
4+
quizzes,
5+
isTeacher,
6+
myQuizResults = {},
7+
onDelete,
8+
onAddClick,
9+
}) {
410
const navigate = useNavigate();
511

612
return (
@@ -121,54 +127,71 @@ function QuizzesTab({ quizzes, isTeacher, myQuizResults = {}, onDelete, onAddCli
121127
</span>
122128
)}
123129
{/* Score pill — shown after student completes quiz */}
124-
{!isTeacher && myQuizResults[q.id] && (() => {
125-
const r = myQuizResults[q.id];
126-
const pct = r.percentage ?? 0;
127-
const colorCls = pct >= 70
128-
? "bg-emerald-500/12 text-emerald-400 border-emerald-500/20"
129-
: pct >= 40
130-
? "bg-amber-500/12 text-amber-400 border-amber-500/20"
131-
: "bg-red-500/12 text-red-400 border-red-500/20";
132-
return (
133-
<span className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-black border ${colorCls}`}>
134-
🏆 {r.score}/{r.totalPoints} pts · {pct}%
135-
</span>
136-
);
137-
})()}
130+
{!isTeacher &&
131+
myQuizResults[q.id] &&
132+
(() => {
133+
const r = myQuizResults[q.id];
134+
const pct = r.percentage ?? 0;
135+
const colorCls =
136+
pct >= 70
137+
? "bg-emerald-500/12 text-emerald-400 border-emerald-500/20"
138+
: pct >= 40
139+
? "bg-amber-500/12 text-amber-400 border-amber-500/20"
140+
: "bg-red-500/12 text-red-400 border-red-500/20";
141+
return (
142+
<span
143+
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-black border ${colorCls}`}
144+
>
145+
🏆 {r.score}/{r.totalPoints} pts · {pct}%
146+
</span>
147+
);
148+
})()}
138149
</div>
139150

140151
{/* Actions */}
141152
<div className="flex items-center gap-2.5">
142-
{!isTeacher && (() => {
143-
const taken = !!myQuizResults[q.id];
144-
return (
145-
<button
146-
onClick={() => !taken && navigate(`/quiz/${q.id}`)}
147-
disabled={taken}
148-
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-bold transition-all active:scale-95
149-
${taken
150-
? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 cursor-not-allowed opacity-80"
151-
: "sc-btn-glow cursor-pointer"
153+
{!isTeacher &&
154+
(() => {
155+
const taken = !!myQuizResults[q.id];
156+
return (
157+
<button
158+
onClick={() => !taken && navigate(`/quiz/${q.id}`)}
159+
disabled={taken}
160+
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-bold transition-all active:scale-95
161+
${
162+
taken
163+
? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 cursor-not-allowed opacity-80"
164+
: "sc-btn-glow cursor-pointer"
152165
}`}
153-
>
154-
{taken ? (
155-
<>
156-
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
157-
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z" />
158-
</svg>
159-
Quiz Taken
160-
</>
161-
) : (
162-
<>
163-
Take Quiz
164-
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
165-
<path d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z" />
166-
</svg>
167-
</>
168-
)}
169-
</button>
170-
);
171-
})()}
166+
>
167+
{taken ? (
168+
<>
169+
<svg
170+
width="12"
171+
height="12"
172+
viewBox="0 0 16 16"
173+
fill="currentColor"
174+
>
175+
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z" />
176+
</svg>
177+
Quiz Taken
178+
</>
179+
) : (
180+
<>
181+
Take Quiz
182+
<svg
183+
width="12"
184+
height="12"
185+
viewBox="0 0 16 16"
186+
fill="currentColor"
187+
>
188+
<path d="M8.22 2.97a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06l2.97-2.97H3.75a.75.75 0 010-1.5h7.44L8.22 4.03a.75.75 0 010-1.06z" />
189+
</svg>
190+
</>
191+
)}
192+
</button>
193+
);
194+
})()}
172195
{isTeacher && (
173196
<>
174197
<button

client/src/pages/CourseView.jsx

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -382,17 +382,26 @@ function CourseView() {
382382
const fd = new FormData();
383383
fd.append("file", assForm.attachmentFile);
384384
fd.append("teacherId", user.id);
385-
const attRes = await apiFetch(`/api/assignments/${data.id}/attachments`, {
386-
method: "POST",
387-
body: fd,
388-
});
385+
const attRes = await apiFetch(
386+
`/api/assignments/${data.id}/attachments`,
387+
{
388+
method: "POST",
389+
body: fd,
390+
},
391+
);
389392
if (attRes.ok) data = await attRes.json();
390393
}
391394
setAssignments((p) =>
392395
p.some((a) => String(a.id) === String(data.id)) ? p : [data, ...p],
393396
);
394397
setModal(null);
395-
setAssForm({ title: "", description: "", dueDate: "", maxScore: 100, attachmentFile: null });
398+
setAssForm({
399+
title: "",
400+
description: "",
401+
dueDate: "",
402+
maxScore: 100,
403+
attachmentFile: null,
404+
});
396405
}
397406
setSaving(false);
398407
};

client/src/pages/LiveClassRoom.jsx

Lines changed: 80 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ export default function LiveClassRoom() {
160160
const [studentCamOn, setStudentCamOn] = useState(false);
161161
const [streamActive, setStreamActive] = useState(false);
162162
const [teacherHasScreen, setTeacherHasScreen] = useState(false);
163+
const [audioBlocked, setAudioBlocked] = useState(false);
163164

164165
// ── student tiles (teacher's view of all connected students) ────────────────
165166
// Map<viewerSocketId, { stream: MediaStream|null, userId, userName, camOn: bool }>
@@ -171,11 +172,13 @@ export default function LiveClassRoom() {
171172
const studentMicStreamRef = useRef(null); // student: mic stream
172173
const studentCamStreamRef = useRef(null); // student: camera stream
173174
const pendingScreenRef = useRef(null); // student: pending screen stream before mount
175+
const screenStreamIdRef = useRef(null); // student: expected screen stream.id from teacher
174176

175-
// ── video element refs ──────────────────────────────────────────────────────
177+
// ── video / audio element refs ──────────────────────────────────────────────
176178
const localCameraRef = useRef(null); // teacher: self camera preview
177179
const localScreenRef = useRef(null); // teacher: self screen preview
178-
const remoteCameraRef = useRef(null); // student: teacher's camera
180+
const remoteCameraRef = useRef(null); // student: teacher's camera (video only)
181+
const remoteAudioRef = useRef(null); // student: teacher's audio (dedicated element)
179182
const remoteScreenRef = useRef(null); // student: teacher's screen
180183
const studentSelfVideoRef = useRef(null); // student: self camera preview
181184

@@ -355,7 +358,10 @@ export default function LiveClassRoom() {
355358
const sender = pc.addTrack(st, stream);
356359
screenSendersRef.current.set(vid, sender);
357360
});
358-
socket.emit("screen-share-started", { liveClassId: id });
361+
socket.emit("screen-share-started", {
362+
liveClassId: id,
363+
screenStreamId: stream.id,
364+
});
359365
st.onended = stopScreenShare;
360366
} catch {
361367
/* user cancelled */
@@ -715,26 +721,52 @@ export default function LiveClassRoom() {
715721
const pc = new RTCPeerConnection(ICE_CONFIG);
716722
peerConnRef.current = pc;
717723

718-
pc.ontrack = ({ track }) => {
724+
pc.ontrack = ({ track, streams }) => {
719725
if (track.kind === "audio") {
720-
const s = remoteCameraRef.current?.srcObject ?? new MediaStream();
721-
s.addTrack(track);
722-
if (remoteCameraRef.current) remoteCameraRef.current.srcObject = s;
726+
// Dedicated audio element — avoids autoplay blocking on the video element
727+
const audioEl = remoteAudioRef.current;
728+
if (audioEl) {
729+
const s =
730+
audioEl.srcObject instanceof MediaStream
731+
? audioEl.srcObject
732+
: new MediaStream();
733+
if (!s.getAudioTracks().find((t) => t.id === track.id)) {
734+
s.addTrack(track);
735+
audioEl.srcObject = s;
736+
audioEl.play().catch(() => setAudioBlocked(true));
737+
}
738+
}
723739
return;
724740
}
725-
if (track.contentHint === "detail") {
726-
const stream = new MediaStream([track]);
727-
pendingScreenRef.current = stream;
741+
// Video: distinguish screen vs camera by stream ID
742+
// (contentHint is a local hint and is NOT transmitted over WebRTC)
743+
const incomingStreamId = streams[0]?.id;
744+
const isScreenTrack =
745+
incomingStreamId &&
746+
screenStreamIdRef.current &&
747+
incomingStreamId === screenStreamIdRef.current;
748+
749+
if (isScreenTrack) {
750+
const screenStream = streams[0] ?? new MediaStream([track]);
751+
pendingScreenRef.current = screenStream;
728752
setTeacherHasScreen(true);
729753
setStreamActive(true);
730754
track.onended = () => {
731755
setTeacherHasScreen(false);
732756
pendingScreenRef.current = null;
757+
if (remoteScreenRef.current)
758+
remoteScreenRef.current.srcObject = null;
733759
};
734760
} else {
735-
const s = remoteCameraRef.current?.srcObject ?? new MediaStream();
736-
s.addTrack(track);
737-
if (remoteCameraRef.current) remoteCameraRef.current.srcObject = s;
761+
// Camera video track
762+
const s =
763+
remoteCameraRef.current?.srcObject instanceof MediaStream
764+
? remoteCameraRef.current.srcObject
765+
: new MediaStream();
766+
if (!s.getVideoTracks().find((t) => t.id === track.id)) {
767+
s.addTrack(track);
768+
if (remoteCameraRef.current) remoteCameraRef.current.srcObject = s;
769+
}
738770
setStreamActive(true);
739771
}
740772
};
@@ -839,13 +871,21 @@ export default function LiveClassRoom() {
839871
const onBroadcasterLeft = () => {
840872
setStreamActive(false);
841873
setTeacherHasScreen(false);
874+
screenStreamIdRef.current = null;
875+
pendingScreenRef.current = null;
842876
if (remoteCameraRef.current) remoteCameraRef.current.srcObject = null;
877+
if (remoteAudioRef.current) remoteAudioRef.current.srcObject = null;
878+
if (remoteScreenRef.current) remoteScreenRef.current.srcObject = null;
843879
peerConnRef.current?.close();
844880
peerConnRef.current = null;
845881
};
846882

847-
const onScreenStarted = () => setTeacherHasScreen(true);
883+
const onScreenStarted = ({ screenStreamId }) => {
884+
if (screenStreamId) screenStreamIdRef.current = screenStreamId;
885+
setTeacherHasScreen(true);
886+
};
848887
const onScreenStopped = () => {
888+
screenStreamIdRef.current = null;
849889
setTeacherHasScreen(false);
850890
if (remoteScreenRef.current) remoteScreenRef.current.srcObject = null;
851891
pendingScreenRef.current = null;
@@ -1086,12 +1126,23 @@ export default function LiveClassRoom() {
10861126
/>
10871127
)}
10881128

1129+
{/* Student: dedicated hidden audio element for teacher voice */}
1130+
{!isTeacher && (
1131+
<audio
1132+
ref={remoteAudioRef}
1133+
autoPlay
1134+
playsInline
1135+
className="hidden"
1136+
/>
1137+
)}
1138+
10891139
{/* Student: teacher's camera */}
10901140
{!isTeacher && (
10911141
<video
10921142
ref={remoteCameraRef}
10931143
autoPlay
10941144
playsInline
1145+
muted
10951146
className={`object-cover
10961147
${
10971148
teacherHasScreen
@@ -1101,6 +1152,21 @@ export default function LiveClassRoom() {
11011152
/>
11021153
)}
11031154

1155+
{/* Student: audio blocked prompt */}
1156+
{!isTeacher && audioBlocked && (
1157+
<button
1158+
onClick={() => {
1159+
remoteAudioRef.current
1160+
?.play()
1161+
.then(() => setAudioBlocked(false))
1162+
.catch(() => {});
1163+
}}
1164+
className="absolute top-4 left-1/2 -translate-x-1/2 z-30 flex items-center gap-2 px-4 py-2.5 rounded-xl bg-amber-500/20 text-amber-300 border border-amber-500/30 text-xs font-bold cursor-pointer hover:bg-amber-500/30 transition-colors animate-pulse"
1165+
>
1166+
🔇 Click to enable audio
1167+
</button>
1168+
)}
1169+
11041170
{/* Student: self-view preview (own camera on) */}
11051171
{!isTeacher && studentCamOn && (
11061172
<div className="absolute bottom-4 left-4 z-20">

0 commit comments

Comments
 (0)