Skip to content
Merged
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
105 changes: 89 additions & 16 deletions client/src/components/CourseView/AssignmentCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function AssignmentCard({
mySubmission,
submissions,
submissionText,
submissionFile,
isLocked,
lockedByTitle,
lockedByOrder,
Expand Down Expand Up @@ -179,6 +180,28 @@ function AssignmentCard({
</div>
)}

{/* Assignment attachments (visible to all) */}
{assignment.attachments?.length > 0 && (
<div className="mt-4 pt-4 border-t border-[var(--border)]/10">
<p className="text-[11px] font-bold text-[var(--muted)] uppercase tracking-[0.12em] mb-2">
📎 Attachments
</p>
<div className="flex flex-wrap gap-2">
{assignment.attachments.map((att) => (
<a
key={att.id}
href={att.url}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-3.5 py-2 bg-[var(--accent)]/8 hover:bg-[var(--accent)]/15 text-[var(--accent)] rounded-xl text-xs font-bold border border-[var(--accent)]/12 transition-all duration-300"
>
{att.name.endsWith(".pdf") ? "📄" : "📝"} {att.name}
</a>
))}
</div>
</div>
)}

{/* Student: submission form */}
{!isTeacher && !hasSubmission && !isLocked && (
<div className="mt-5 pt-5 border-t border-[var(--border)]">
Expand All @@ -189,17 +212,41 @@ function AssignmentCard({
className="w-full px-4 py-3.5 bg-[var(--surface-elevated)] border border-[var(--border)] rounded-2xl text-sm outline-none
resize-y focus:border-[var(--accent)] focus:ring-2 focus:ring-[var(--accent)]/12
transition-all duration-300 text-[var(--text)] placeholder:text-[var(--muted)] min-h-[100px] font-medium"
placeholder="Write your answer here..."
placeholder="Write your answer here... (optional if uploading a file)"
value={submissionText || ""}
onChange={(e) => onSubmit(assignment.id, e.target.value, true)}
onChange={(e) => onSubmit(assignment.id, e.target.value, "text")}
/>
{/* File upload */}
<div className="mt-3">
<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">
<span className="text-lg">📎</span>
<span className="text-sm text-[var(--muted)] flex-1 truncate">
{submissionFile ? submissionFile.name : "Attach PDF or DOCX (optional)"}
</span>
<input
type="file"
accept=".pdf,.doc,.docx"
className="hidden"
onChange={(e) => onSubmit(assignment.id, e.target.files[0] || null, "file")}
/>
</label>
{submissionFile && (
<button
type="button"
onClick={() => onSubmit(assignment.id, null, "file")}
className="mt-1 ml-1 text-[11px] text-red-400 hover:text-red-300 cursor-pointer"
>
Remove file
</button>
)}
</div>
<div className="flex justify-end mt-3">
<button
onClick={() => onSubmit(assignment.id, submissionText, false)}
disabled={!submissionText?.trim()}
onClick={() => onSubmit(assignment.id, null, "submit")}
disabled={!submissionText?.trim() && !submissionFile}
className="px-6 py-2.5 sc-btn-glow disabled:opacity-35 rounded-xl text-xs font-bold cursor-pointer disabled:cursor-not-allowed transition-all active:scale-95"
>
Submit Answer
Submit →
</button>
</div>
</div>
Expand All @@ -212,9 +259,21 @@ function AssignmentCard({
Your Submission
</p>
<div className="glass rounded-2xl px-5 py-4 border border-[var(--border)]/12">
<p className="text-sm text-[var(--text)] leading-relaxed">
{mySubmission.content}
</p>
{mySubmission.content && (
<p className="text-sm text-[var(--text)] leading-relaxed">
{mySubmission.content}
</p>
)}
{mySubmission.fileUrl && (
<a
href={mySubmission.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 mt-2 px-3.5 py-2 bg-[var(--accent)]/8 hover:bg-[var(--accent)]/15 text-[var(--accent)] rounded-xl text-xs font-bold border border-[var(--accent)]/12 transition-all duration-300"
>
📎 {mySubmission.fileName || "Download file"}
</a>
)}
{mySubmission.feedback && (
<div className="mt-4 pt-4 border-t border-[var(--border)]/12">
<p className="text-[11px] font-bold text-[var(--accent)] uppercase tracking-[0.12em] mb-1.5 flex items-center gap-1.5">
Expand Down Expand Up @@ -286,14 +345,28 @@ function AssignmentCard({
</button>
</div>
</div>
<p className="text-[13px] text-[var(--text)] leading-relaxed pl-[42px]">
{s.content}
</p>
{s.feedback && (
<p className="text-[11px] text-[var(--muted)] mt-1.5 pl-[42px] italic">
"{s.feedback}"
</p>
)}
<div className="pl-[42px]">
{s.content && (
<p className="text-[13px] text-[var(--text)] leading-relaxed">
{s.content}
</p>
)}
{s.fileUrl && (
<a
href={s.fileUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 mt-1.5 px-3 py-1.5 bg-[var(--accent)]/8 hover:bg-[var(--accent)]/15 text-[var(--accent)] rounded-xl text-[11px] font-bold border border-[var(--accent)]/12 transition-all duration-300"
>
📎 {s.fileName || "View file"}
</a>
)}
{s.feedback && (
<p className="text-[11px] text-[var(--muted)] mt-1.5 italic">
"{s.feedback}"
</p>
)}
</div>
</div>
);
})}
Expand Down
34 changes: 34 additions & 0 deletions client/src/components/CourseView/AssignmentModal.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ function AssignmentModal({
}) {
if (!isOpen) return null;

const handleFileChange = (e) => {
const file = e.target.files[0] || null;
onChange({ ...form, attachmentFile: file });
};

return modalOverlay(
onClose,
<>
Expand Down Expand Up @@ -68,6 +73,35 @@ function AssignmentModal({
/>
</div>
</div>

{/* Optional document attachment */}
<div>
<label className="block text-[11px] font-bold text-[var(--text)] uppercase tracking-wider mb-2 ml-1 opacity-80">
Attach Document <span className="normal-case font-normal opacity-60">(PDF or DOCX, optional)</span>
</label>
<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">
<span className="text-xl">📎</span>
<span className="text-sm text-[var(--muted)] flex-1 truncate">
{form.attachmentFile ? form.attachmentFile.name : "Click to choose file…"}
</span>
<input
type="file"
accept=".pdf,.doc,.docx,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
className="hidden"
onChange={handleFileChange}
/>
</label>
{form.attachmentFile && (
<button
type="button"
onClick={() => onChange({ ...form, attachmentFile: null })}
className="mt-1.5 ml-1 text-[11px] text-red-400 hover:text-red-300 cursor-pointer"
>
Remove file
</button>
)}
</div>

<div className="flex justify-end gap-3 pt-6 mt-2 border-t border-[var(--border)]/30">
<button
type="button"
Expand Down
6 changes: 3 additions & 3 deletions client/src/components/CourseView/AssignmentsTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ function AssignmentsTab({
mySubmissions,
expandedSubs,
submissionText,
submissionFile,
onSubmit,
onToggleSubs,
onDelete,
Expand Down Expand Up @@ -119,12 +120,11 @@ function AssignmentsTab({
mySubmission={mySubmissions[a.id]}
submissions={expandedSubs[a.id]}
submissionText={submissionText[a.id]}
submissionFile={submissionFile?.[a.id]}
isLocked={!!lockInfo}
lockedByTitle={lockInfo?.byTitle}
lockedByOrder={lockInfo?.byOrder}
onSubmit={(aid, text, isUpdate) =>
isUpdate ? onSubmit(aid, text) : onSubmit(aid, text, false)
}
onSubmit={onSubmit}
onToggleSubs={onToggleSubs}
onDelete={onDelete}
onGrade={onGrade}
Expand Down
63 changes: 46 additions & 17 deletions client/src/components/CourseView/QuizzesTab.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useNavigate } from "react-router-dom";

function QuizzesTab({ quizzes, isTeacher, onDelete, onAddClick }) {
function QuizzesTab({ quizzes, isTeacher, myQuizResults = {}, onDelete, onAddClick }) {
const navigate = useNavigate();

return (
Expand Down Expand Up @@ -120,26 +120,55 @@ function QuizzesTab({ quizzes, isTeacher, onDelete, onAddClick }) {
})}
</span>
)}
{/* Score pill — shown after student completes quiz */}
{!isTeacher && myQuizResults[q.id] && (() => {
const r = myQuizResults[q.id];
const pct = r.percentage ?? 0;
const colorCls = pct >= 70
? "bg-emerald-500/12 text-emerald-400 border-emerald-500/20"
: pct >= 40
? "bg-amber-500/12 text-amber-400 border-amber-500/20"
: "bg-red-500/12 text-red-400 border-red-500/20";
return (
<span className={`flex items-center gap-1.5 px-3 py-1.5 rounded-xl text-[11px] font-black border ${colorCls}`}>
🏆 {r.score}/{r.totalPoints} pts · {pct}%
</span>
);
})()}
</div>

{/* Actions */}
<div className="flex gap-2.5">
{!isTeacher && (
<button
onClick={() => navigate(`/quiz/${q.id}`)}
className="flex items-center gap-2 px-5 py-2.5 sc-btn-glow rounded-xl text-xs font-bold cursor-pointer active:scale-95"
>
Take Quiz
<svg
width="12"
height="12"
viewBox="0 0 16 16"
fill="currentColor"
<div className="flex items-center gap-2.5">
{!isTeacher && (() => {
const taken = !!myQuizResults[q.id];
return (
<button
onClick={() => !taken && navigate(`/quiz/${q.id}`)}
disabled={taken}
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-xs font-bold transition-all active:scale-95
${taken
? "bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 cursor-not-allowed opacity-80"
: "sc-btn-glow cursor-pointer"
}`}
>
<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" />
</svg>
</button>
)}
{taken ? (
<>
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
<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" />
</svg>
Quiz Taken
</>
) : (
<>
Take Quiz
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
<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" />
</svg>
</>
)}
</button>
);
})()}
{isTeacher && (
<>
<button
Expand Down
Loading
Loading