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
69 changes: 57 additions & 12 deletions client/src/components/CourseView/AssignmentCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ function AssignmentCard({
mySubmission,
submissions,
submissionText,
isLocked,
lockedByTitle,
lockedByOrder,
onSubmit,
onToggleSubs,
onDelete,
Expand All @@ -22,8 +25,13 @@ function AssignmentCard({
const hasSubmission = !!mySubmission;
const isGraded = mySubmission?.status === "graded";

const statusConfig =
isOverdue && !hasSubmission
const statusConfig = isLocked
? {
bar: "from-slate-500 to-slate-600",
iconBg: "bg-slate-500/12 border-slate-500/18",
icon: "🔒",
}
: isOverdue && !hasSubmission
? {
bar: "from-red-500 to-red-600",
iconBg: "bg-red-500/12 border-red-500/18",
Expand All @@ -43,17 +51,32 @@ function AssignmentCard({

return (
<div
className="group bg-[var(--surface)] rounded-2xl border border-[var(--border)] hover:border-[var(--accent)]/40 transition-all duration-300 overflow-hidden
hover:shadow-[0_16px_48px_-12px_rgba(0,0,0,0.12)]"
className={`group bg-[var(--surface)] rounded-2xl border transition-all duration-300 overflow-hidden
${
isLocked
? "border-[var(--border)]/40 opacity-70"
: "border-[var(--border)] hover:border-[var(--accent)]/40 hover:shadow-[0_16px_48px_-12px_rgba(0,0,0,0.12)]"
}`}
>
{/* Sequential order indicator */}
<div
className={`h-0.5 w-full bg-gradient-to-r ${statusConfig.bar} opacity-60`}
/>

<div className="p-5 sm:p-6">
<div className="flex items-start gap-4">
{/* Status icon */}
<div
className={`w-12 h-12 rounded-2xl flex items-center justify-center text-xl shrink-0 border
${statusConfig.iconBg} group-hover:scale-105 transition-transform duration-300`}
>
{statusConfig.icon}
<div className="flex flex-col items-center gap-1.5">
<div
className={`w-12 h-12 rounded-2xl flex items-center justify-center text-xl shrink-0 border
${statusConfig.iconBg} ${!isLocked ? "group-hover:scale-105 transition-transform duration-300" : ""}`}
>
{statusConfig.icon}
</div>
{/* Order badge */}
<span className="text-[9px] font-black text-[var(--muted)] bg-[var(--border)]/15 px-1.5 py-0.5 rounded-md uppercase tracking-wider">
#{assignment.order ?? "—"}
</span>
</div>

<div className="flex-1 min-w-0">
Expand All @@ -67,19 +90,23 @@ function AssignmentCard({
<>
<button
onClick={() => onToggleSubs(assignment.id)}
className="px-4 py-2 bg-[var(--accent)]/8 hover:bg-[var(--accent)]/15 text-[var(--accent)] rounded-xl text-xs font-bold
className="px-4 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 cursor-pointer transition-all duration-300 active:scale-95"
>
{submissions !== undefined ? "Hide" : "Submissions"}
</button>
<button
onClick={() => onDelete(assignment.id)}
className="px-4 py-2 bg-red-500/6 hover:bg-red-500/14 text-red-400 rounded-xl text-xs font-bold
className="px-4 py-2 bg-red-500/6 hover:bg-red-500/14 text-red-400 rounded-xl text-xs font-bold
border border-red-500/15 cursor-pointer transition-all duration-300 active:scale-95"
>
Delete
</button>
</>
) : isLocked ? (
<span className="flex items-center gap-1.5 px-3.5 py-1.5 bg-slate-500/10 text-slate-400 rounded-xl text-xs font-bold border border-slate-500/18">
🔒 Locked
</span>
) : isGraded ? (
<span
className={`px-3.5 py-1.5 rounded-xl text-xs font-black border ${scoreColor(scorePct(mySubmission.score, assignment.maxScore))}`}
Expand Down Expand Up @@ -134,8 +161,26 @@ function AssignmentCard({
</div>
</div>

{/* Locked notice for student */}
{!isTeacher && isLocked && (
<div className="mt-5 pt-5 border-t border-[var(--border)]/10">
<div className="flex items-center gap-3 px-4 py-3.5 rounded-2xl bg-slate-500/6 border border-slate-500/15">
<span className="text-xl">🔒</span>
<div>
<p className="text-[13px] font-bold text-[var(--text)]">
Complete Assignment #{lockedByOrder} first
</p>
<p className="text-[12px] text-[var(--muted)] mt-0.5">
Submit "{lockedByTitle}" before you can work on this
assignment.
</p>
</div>
</div>
</div>
)}

{/* Student: submission form */}
{!isTeacher && !hasSubmission && (
{!isTeacher && !hasSubmission && !isLocked && (
<div className="mt-5 pt-5 border-t border-[var(--border)]">
<p className="text-[11px] font-bold text-[var(--muted)] mb-2.5 uppercase tracking-[0.12em]">
Your Answer
Expand Down
79 changes: 58 additions & 21 deletions client/src/components/CourseView/AssignmentsTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,34 @@ function AssignmentsTab({
onGrade,
onAddClick,
}) {
// Compute which assignments are locked for the student.
// An assignment is locked if any previous-order assignment has not been submitted yet
// AND its due date has not passed. Overdue unsubmitted assignments auto-unlock the next one.
const lockedMap = {};
if (!isTeacher) {
// assignments are already sorted by order ascending from the backend
const now = new Date();
let firstUnsubmitted = null;
for (const a of assignments) {
if (firstUnsubmitted !== null) {
// All assignments after the first unsubmitted (non-overdue) one are locked
lockedMap[a.id] = {
locked: true,
byTitle: firstUnsubmitted.title,
byOrder: firstUnsubmitted.order,
};
} else {
const isSubmitted = !!mySubmissions[a.id];
const isOverdue = a.dueDate && new Date(a.dueDate) < now;
if (!isSubmitted && !isOverdue) {
// First unsubmitted assignment that's still within due date — student works on this
firstUnsubmitted = a;
}
// If submitted OR overdue with no submission → treat as passable, don't lock next ones
}
}
}

return (
<div>
{/* Section header */}
Expand All @@ -27,6 +55,9 @@ function AssignmentsTab({
<p className="text-[11px] text-[var(--muted)] font-medium mt-0.5">
{assignments.length}{" "}
{assignments.length === 1 ? "assignment" : "assignments"} posted
{!isTeacher && assignments.length > 0 && (
<span className="ml-2 text-amber-400">· complete in order</span>
)}
</p>
</div>
</div>
Expand Down Expand Up @@ -74,27 +105,33 @@ function AssignmentsTab({
</div>
) : (
<div className="space-y-4">
{assignments.map((a, i) => (
<div
key={a.id}
className="animate-[slide-up_0.4s_cubic-bezier(0.16,1,0.3,1)_both]"
style={{ animationDelay: `${i * 60}ms` }}
>
<AssignmentCard
assignment={a}
isTeacher={isTeacher}
mySubmission={mySubmissions[a.id]}
submissions={expandedSubs[a.id]}
submissionText={submissionText[a.id]}
onSubmit={(aid, text, isUpdate) =>
isUpdate ? onSubmit(aid, text) : onSubmit(aid, text, false)
}
onToggleSubs={onToggleSubs}
onDelete={onDelete}
onGrade={onGrade}
/>
</div>
))}
{assignments.map((a, i) => {
const lockInfo = lockedMap[a.id];
return (
<div
key={a.id}
className="animate-[slide-up_0.4s_cubic-bezier(0.16,1,0.3,1)_both]"
style={{ animationDelay: `${i * 60}ms` }}
>
<AssignmentCard
assignment={a}
isTeacher={isTeacher}
mySubmission={mySubmissions[a.id]}
submissions={expandedSubs[a.id]}
submissionText={submissionText[a.id]}
isLocked={!!lockInfo}
lockedByTitle={lockInfo?.byTitle}
lockedByOrder={lockInfo?.byOrder}
onSubmit={(aid, text, isUpdate) =>
isUpdate ? onSubmit(aid, text) : onSubmit(aid, text, false)
}
onToggleSubs={onToggleSubs}
onDelete={onDelete}
onGrade={onGrade}
/>
</div>
);
})}
</div>
)}
</div>
Expand Down
32 changes: 25 additions & 7 deletions client/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ const NAV_LINKS = [
path: `/ai-playground/${t.id}`,
})),
},
{ label: "Live Classes", icon: "📹", path: "/live-classes" },
];

function Navbar({ showBack }) {
Expand All @@ -68,7 +67,6 @@ function Navbar({ showBack }) {
const mobileMenuRef = useRef(null);
const navDropdownRef = useRef(null);

const themeKeys = Object.keys(themeMap || {});
const unread = notifs.filter((n) => !n.read).length;
const isAuthenticated = user && user.role !== "guest";

Expand Down Expand Up @@ -312,11 +310,14 @@ function Navbar({ showBack }) {
{/* AUTHENTICATED USER SECTION */}
{isAuthenticated ? (
<>
{/* User Info - Desktop Only */}
<div
{/* User Info - Desktop Only (clickable → profile) */}
<button
type="button"
onClick={() => navigate(`/profile`)}
className="hidden lg:flex items-center gap-3 px-3 py-1.5 rounded-xl
border border-[var(--accent)]/20 bg-[var(--accent)]/6
hover:bg-[var(--accent)]/10 transition-all duration-300"
hover:bg-[var(--accent)]/10 transition-all duration-300 cursor-pointer active:scale-95"
title="View your profile"
>
<div className="text-right">
<p className="text-sm font-bold text-[var(--text)] leading-tight">
Expand All @@ -327,13 +328,13 @@ function Navbar({ showBack }) {
</p>
</div>
<div
className="w-9 h-9 rounded-xl bg-gradient-to-br from-[var(--accent)] to-[color-mix(in_srgb,var(--accent)_60%,#ec4899)]
className="w-9 h-9 rounded-xl bg-gradient-to-br from-[var(--accent)] to-[color-mix(in_srgb,var(--accent)_60%,#ec4899)]
flex items-center justify-center text-[var(--accent-contrast)] text-xs font-bold
shadow-[0_4px_12px_-4px_var(--accent)] ring-2 ring-[var(--accent)]/20"
>
{user.name?.charAt(0).toUpperCase()}
</div>
</div>
</button>

{/* Notifications */}
<div className="relative" ref={ref}>
Expand Down Expand Up @@ -511,6 +512,23 @@ function Navbar({ showBack }) {
</button>
);
})}
<button
type="button"
onClick={() => {
navigate("/profile");
setMobileMenuOpen(false);
}}
className={`w-full text-left px-4 py-2.5 rounded-xl text-sm font-semibold flex items-center gap-2.5
transition-all duration-300 active:scale-95 border-none cursor-pointer
${
location.pathname === "/profile"
? "bg-[var(--accent)]/15 text-[var(--accent)]"
: "text-[var(--text)] hover:bg-[var(--accent)]/8"
}`}
>
<span>👤</span>
<span>My Profile</span>
</button>
</div>
<div className="p-2">
<button
Expand Down
Loading
Loading