From aeff60e22d71c80e3649dca8ac63a0422385839f Mon Sep 17 00:00:00 2001 From: nirakarpatel Date: Thu, 16 Jul 2026 20:49:10 +0530 Subject: [PATCH 1/5] Add filtering and export features, fix review mode local storage bug --- admin-client/src/pages/admin/MembersTab.jsx | 95 +++++++++++++- client/src/pages/ChallengeDetails.jsx | 6 +- client/src/pages/chief/ChiefDashboardTab.jsx | 68 +++++++++- client/src/pages/chief/ChiefMembersTab.jsx | 127 ++++++++++++++++--- 4 files changed, 269 insertions(+), 27 deletions(-) diff --git a/admin-client/src/pages/admin/MembersTab.jsx b/admin-client/src/pages/admin/MembersTab.jsx index 03a921f..b4eb120 100644 --- a/admin-client/src/pages/admin/MembersTab.jsx +++ b/admin-client/src/pages/admin/MembersTab.jsx @@ -15,6 +15,9 @@ const MembersTab = ({ initialClanFilter }) => { const [search, setSearch] = useState(''); const [levelFilter, setLevelFilter] = useState(''); const [clanFilter, setClanFilter] = useState(initialClanFilter || ''); + const [minSolved, setMinSolved] = useState(''); + const [maxSolved, setMaxSolved] = useState(''); + const [sortBy, setSortBy] = useState('Default'); const [menuOpen, setMenuOpen] = useState(null); const [menuPos, setMenuPos] = useState({ top: 0, right: 0 }); const canManageUsers = canManageClanGlobally(user); @@ -184,9 +187,55 @@ const MembersTab = ({ initialClanFilter }) => { } else if (clanFilter) { matchClan = activeClan?._id === clanFilter; } - return matchSearch && matchLevel && matchClan; + + const solved = u.solvedProblems || 0; + const matchesMin = minSolved === '' || solved >= parseInt(minSolved); + const matchesMax = maxSolved === '' || solved <= parseInt(maxSolved); + + return matchSearch && matchLevel && matchClan && matchesMin && matchesMax; + }); + + const sortedUsers = [...filteredUsers].sort((a, b) => { + const aSolved = a.solvedProblems || 0; + const bSolved = b.solvedProblems || 0; + if (sortBy === 'Solved (Low to High)') return aSolved - bSolved; + if (sortBy === 'Solved (High to Low)') return bSolved - aSolved; + return 0; // Default }); + const handleDownloadCSV = () => { + const headers = ['Name', 'Username', 'Email', 'Reg No', 'Clan', 'Level', 'Solved', 'XP', 'Streak', 'Status']; + const rows = sortedUsers.map(u => { + const activeClan = u.clan ? (clansQuery.data || []).find(c => c._id === u.clan || c._id === u.clan?._id) : null; + const clanName = activeClan ? activeClan.name : 'Unassigned'; + let status = 'Not Started'; + if ((u.solvedProblems || 0) > 0) status = 'In Progress'; // Can't easily determine 'Completed all' globally without total question count + + return [ + `"${u.name || ''}"`, + `"${u.username || ''}"`, + `"${u.email || ''}"`, + `"${u.regNo || 'N/A'}"`, + `"${clanName}"`, + `"${u.codingLevel || 'Beginner'}"`, + u.solvedProblems || 0, + u.points || 0, + u.streak || 0, + `"${u.status || 'Active'}"` + ].join(','); + }); + + const csvContent = [headers.join(','), ...rows].join('\n'); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', url); + link.setAttribute('download', `Admin_Directory_${new Date().toISOString().split('T')[0]}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + const getStatusDot = (status) => { switch(status) { case 'Active': return
; @@ -250,6 +299,44 @@ const MembersTab = ({ initialClanFilter }) => { ))}
+ +
+ Solved: + setMinSolved(e.target.value)} + className="w-12 bg-transparent text-sm text-black dark:text-white text-center focus:outline-none border-b border-transparent focus:border-blue-500/50" + /> + - + setMaxSolved(e.target.value)} + className="w-12 bg-transparent text-sm text-black dark:text-white text-center focus:outline-none border-b border-transparent focus:border-blue-500/50" + /> +
+ +
+ +
+ + @@ -352,7 +439,7 @@ const MembersTab = ({ initialClanFilter }) => { - {filteredUsers.map((user, i) => ( + {sortedUsers.map((user, i) => ( @@ -413,14 +500,14 @@ const MembersTab = ({ initialClanFilter }) => { ))} - {filteredUsers.length === 0 && ( + {sortedUsers.length === 0 && ( No members found matching the criteria. )}
- Showing {filteredUsers.length} members + Showing {sortedUsers.length} members
diff --git a/client/src/pages/ChallengeDetails.jsx b/client/src/pages/ChallengeDetails.jsx index 7243f7d..bc0f917 100644 --- a/client/src/pages/ChallengeDetails.jsx +++ b/client/src/pages/ChallengeDetails.jsx @@ -228,6 +228,7 @@ const ChallengeDetails = () => { // Logic: LocalStorage Persistence useEffect(() => { + if (isReviewMode) return; const raw = localStorage.getItem(draftKey); if (!raw) return; try { @@ -240,7 +241,7 @@ const ChallengeDetails = () => { } catch { localStorage.removeItem(draftKey); } - }, [draftKey]); + }, [draftKey, isReviewMode]); const challengeQuery = useQuery({ queryKey: ["challenge", id], @@ -259,6 +260,7 @@ const ChallengeDetails = () => { }); useEffect(() => { + if (isReviewMode) return; const hasAnyCode = Object.values(codeByLang).some((c) => c.trim()); if (!(repoUrl.trim() || hasAnyCode)) { localStorage.removeItem(draftKey); @@ -277,7 +279,7 @@ const ChallengeDetails = () => { updatedAt: new Date().toISOString(), }), ); - }, [draftKey, repoUrl, codeByLang, language, challengeQuery.data]); + }, [draftKey, repoUrl, codeByLang, language, challengeQuery.data, isReviewMode]); // A language is offered only if this challenge can actually run in it. // Python/JS drivers are dynamic; compiled languages need a drivable signature. diff --git a/client/src/pages/chief/ChiefDashboardTab.jsx b/client/src/pages/chief/ChiefDashboardTab.jsx index a008280..340bb2d 100644 --- a/client/src/pages/chief/ChiefDashboardTab.jsx +++ b/client/src/pages/chief/ChiefDashboardTab.jsx @@ -49,6 +49,7 @@ const ChiefDashboardTab = ({ clan, onTabChange }) => { const [warningModal, setWarningModal] = useState({ open: false, user: null, message: '' }); const [selectedSetId, setSelectedSetId] = useState(null); const [hoveredMember, setHoveredMember] = useState(null); // { memberId, name, x, y } + const [filterType, setFilterType] = useState('All'); // Filter state const isArchived = isClanArchived(clan); const canManageClan = canManageOwnClan(user, clan); const canArchive = canArchiveClan(user, clan); @@ -178,6 +179,50 @@ const ChiefDashboardTab = ({ clan, onTabChange }) => { const circleCircumference = 2 * Math.PI * circleRadius; const strokeDashoffset = circleCircumference - (completionRate / 100) * circleCircumference; + const filteredMembers = members.filter(member => { + if (filterType === 'All') return true; + const setData = selectedMembers[member._id]; + const solved = setData?.solved ?? 0; + const total = setData?.total ?? (selectedSet?.challengeCount ?? 0); + if (filterType === 'Completed') return total > 0 && solved === total; + if (filterType === 'In Progress') return solved > 0 && solved < total; + if (filterType === 'Not Started') return solved === 0; + return true; + }); + + const handleDownloadCSV = () => { + const headers = ['Name', 'Username', 'Email', 'Level', 'XP', 'Solved', 'Total', 'Status']; + const rows = filteredMembers.map(member => { + const setData = selectedMembers[member._id]; + const solved = setData?.solved ?? 0; + const total = setData?.total ?? (selectedSet?.challengeCount ?? 0); + let status = 'Not Started'; + if (total > 0 && solved === total) status = 'Completed'; + else if (solved > 0) status = 'In Progress'; + + return [ + `"${member.name || ''}"`, + `"${member.username || ''}"`, + `"${member.email || ''}"`, + `"${member.codingLevel || 'Beginner'}"`, + member.points || 0, + solved, + total, + `"${status}"` + ].join(','); + }); + + const csvContent = [headers.join(','), ...rows].join('\n'); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', url); + link.setAttribute('download', `Clan_Progress_${clan.name?.replace(/\s+/g, '_')}_${selectedSet?.title?.replace(/\s+/g, '_') || 'Overview'}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + return (
{isArchived && ( @@ -296,10 +341,29 @@ const ChiefDashboardTab = ({ clan, onTabChange }) => { {/* Right Col: Member Progress List */} -
+

Member Progress Overview

+
+ + +
{/* Set selector */} @@ -332,7 +396,7 @@ const ChiefDashboardTab = ({ clan, onTabChange }) => { {!setAnalyticsQuery.isLoading && sets.length === 0 && (

No question sets published yet.

)} - {sets.length > 0 && members.map((member) => { + {sets.length > 0 && filteredMembers.map((member) => { const { text: timeText, isOnline } = getRelativeTime(member.lastLoginDate || member.createdAt); const isWarned = member.status === 'Warned'; const setData = selectedMembers[member._id]; diff --git a/client/src/pages/chief/ChiefMembersTab.jsx b/client/src/pages/chief/ChiefMembersTab.jsx index f8199fe..27f15e8 100644 --- a/client/src/pages/chief/ChiefMembersTab.jsx +++ b/client/src/pages/chief/ChiefMembersTab.jsx @@ -19,6 +19,9 @@ const ChiefMembersTab = ({ clan }) => { const [warnMessage, setWarnMessage] = useState(''); const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('All'); + const [minSolved, setMinSolved] = useState(''); + const [maxSolved, setMaxSolved] = useState(''); + const [sortBy, setSortBy] = useState('Default'); const isArchived = isClanArchived(clan); const canManageMembers = canManageClanMembers(currentUser, clan); const canManageClan = canManageOwnClan(currentUser, clan); @@ -104,29 +107,85 @@ const ChiefMembersTab = ({ clan }) => { (member.regNo && member.regNo.toLowerCase().includes(searchTerm.toLowerCase())) || (member.email && member.email.toLowerCase().includes(searchTerm.toLowerCase())); const memberStatus = member.status || 'Active'; - if (statusFilter === 'All') return matchesSearch; - return matchesSearch && memberStatus === statusFilter; + const statusMatch = statusFilter === 'All' ? true : memberStatus === statusFilter; + + const solved = member.solvedProblems || 0; + const matchesMin = minSolved === '' || solved >= parseInt(minSolved); + const matchesMax = maxSolved === '' || solved <= parseInt(maxSolved); + + return matchesSearch && statusMatch && matchesMin && matchesMax; }); + const sortedMembers = [...filteredMembers].sort((a, b) => { + const aSolved = a.solvedProblems || 0; + const bSolved = b.solvedProblems || 0; + if (sortBy === 'Solved (Low to High)') return aSolved - bSolved; + if (sortBy === 'Solved (High to Low)') return bSolved - aSolved; + return 0; // Default + }); + + const handleDownloadCSV = () => { + const headers = ['Name', 'Username', 'Email', 'Reg No', 'Level', 'Solved', 'XP', 'Streak', 'Status']; + const rows = sortedMembers.map(user => { + const isWarned = user.status === 'Warned'; + const isInactive = user.status === 'Inactive'; + const status = isWarned ? 'Warned' : isInactive ? 'Inactive' : 'Active'; + + return [ + `"${user.name || ''}"`, + `"${user.username || ''}"`, + `"${user.email || ''}"`, + `"${user.regNo || 'N/A'}"`, + `"${user.codingLevel || 'Beginner'}"`, + user.solvedProblems || 0, + user.points || 0, + user.streak || 0, + `"${status}"` + ].join(','); + }); + + const csvContent = [headers.join(','), ...rows].join('\n'); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.setAttribute('href', url); + link.setAttribute('download', `Member_Management_${clan.name?.replace(/\s+/g, '_') || 'Clan'}.csv`); + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + return (
-
-

- Member Management -

-
-
- - setSearchTerm(e.target.value)} - className="field-input pl-10 h-10 text-sm w-full" - /> +
+
+

+ Member Management +

+
+
+ + setSearchTerm(e.target.value)} + className="field-input pl-10 h-10 text-sm w-full" + /> +
+ +
+
+ +
+ +
+ Solved: + setMinSolved(e.target.value)} + className="w-16 h-8 bg-transparent text-sm text-primary text-center focus:outline-none border-b border-transparent focus:border-accent" + /> + - + setMaxSolved(e.target.value)} + className="w-16 h-8 bg-transparent text-sm text-primary text-center focus:outline-none border-b border-transparent focus:border-accent" + /> +
+ +
@@ -153,7 +242,7 @@ const ChiefMembersTab = ({ clan }) => { - {filteredMembers.map((user, i) => { + {sortedMembers.map((user, i) => { const isWarned = user.status === 'Warned'; const isInactive = user.status === 'Inactive'; const isActive = !isWarned && !isInactive; @@ -274,7 +363,7 @@ const ChiefMembersTab = ({ clan }) => { ); })} - {filteredMembers.length === 0 && ( + {sortedMembers.length === 0 && ( No members found matching your filters. From 22c8b1b324f38a8e1b0384d354b96829fa12dc7b Mon Sep 17 00:00:00 2001 From: nirakarpatel Date: Sat, 18 Jul 2026 13:40:49 +0530 Subject: [PATCH 2/5] fix: CSV name fallback and missing populated name --- client/src/pages/chief/ChiefDashboardTab.jsx | 2 +- client/src/pages/chief/ChiefMembersTab.jsx | 2 +- server/src/features/clans/clan.controller.js | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/client/src/pages/chief/ChiefDashboardTab.jsx b/client/src/pages/chief/ChiefDashboardTab.jsx index 340bb2d..e5b9124 100644 --- a/client/src/pages/chief/ChiefDashboardTab.jsx +++ b/client/src/pages/chief/ChiefDashboardTab.jsx @@ -201,7 +201,7 @@ const ChiefDashboardTab = ({ clan, onTabChange }) => { else if (solved > 0) status = 'In Progress'; return [ - `"${member.name || ''}"`, + `"${member.name || member.username || ''}"`, `"${member.username || ''}"`, `"${member.email || ''}"`, `"${member.codingLevel || 'Beginner'}"`, diff --git a/client/src/pages/chief/ChiefMembersTab.jsx b/client/src/pages/chief/ChiefMembersTab.jsx index 27f15e8..7fc63c1 100644 --- a/client/src/pages/chief/ChiefMembersTab.jsx +++ b/client/src/pages/chief/ChiefMembersTab.jsx @@ -132,7 +132,7 @@ const ChiefMembersTab = ({ clan }) => { const status = isWarned ? 'Warned' : isInactive ? 'Inactive' : 'Active'; return [ - `"${user.name || ''}"`, + `"${user.name || user.username || ''}"`, `"${user.username || ''}"`, `"${user.email || ''}"`, `"${user.regNo || 'N/A'}"`, diff --git a/server/src/features/clans/clan.controller.js b/server/src/features/clans/clan.controller.js index e0dbb6c..d4d9d11 100644 --- a/server/src/features/clans/clan.controller.js +++ b/server/src/features/clans/clan.controller.js @@ -673,7 +673,7 @@ const updateClan = async (req, res, next) => { const populated = await Clan.findById(clan._id) .populate('chief', 'username email') - .populate('members', 'username email profilePicture points streak') + .populate('members', 'name username email profilePicture points streak') .populate('createdBy', 'username email') .populate('archivedBy', 'username email') .populate('restoredBy', 'username email'); @@ -730,7 +730,7 @@ const archiveClan = async (req, res, next) => { const clan = await Clan.findById(req.params.id) .populate('chief', 'username email') - .populate('members', 'username email profilePicture points streak') + .populate('members', 'name username email profilePicture points streak') .populate('createdBy', 'username email') .populate('archivedBy', 'username email') .populate('restoredBy', 'username email'); @@ -762,7 +762,7 @@ const restoreClan = async (req, res, next) => { const populated = await Clan.findById(clan._id) .populate('chief', 'username email') - .populate('members', 'username email profilePicture points streak') + .populate('members', 'name username email profilePicture points streak') .populate('createdBy', 'username email') .populate('archivedBy', 'username email') .populate('restoredBy', 'username email'); @@ -963,7 +963,7 @@ const assignChief = async (req, res, next) => { const populated = await Clan.findById(clanId) .populate('chief', 'username email') - .populate('members', 'username email profilePicture points streak'); + .populate('members', 'name username email profilePicture points streak'); sendSuccess(res, { data: populated, message: 'Clan chief assigned' }); @@ -1031,7 +1031,7 @@ const removeChief = async (req, res, next) => { const populated = await Clan.findById(clanId) .populate('chief', 'username email') - .populate('members', 'username email'); + .populate('members', 'name username email'); return sendSuccess(res, { data: populated, message: 'Clan chief removed' }); } catch (err) { @@ -1099,7 +1099,7 @@ const addMember = async (req, res, next) => { const populated = await Clan.findById(clanId) .populate('chief', 'username email') - .populate('members', 'username email profilePicture points streak'); + .populate('members', 'name username email profilePicture points streak'); sendSuccess(res, { data: populated, message: 'Member added' }); @@ -1182,7 +1182,7 @@ const removeMember = async (req, res, next) => { const populated = await Clan.findById(clanId) .populate('chief', 'username email') - .populate('members', 'username email profilePicture points streak'); + .populate('members', 'name username email profilePicture points streak'); return sendSuccess(res, { data: populated, message: 'Member removed' }); } catch (err) { From 60751fcd625d084c2ebaab386621a27cfa7e2080 Mon Sep 17 00:00:00 2001 From: nirakarpatel Date: Sat, 18 Jul 2026 13:53:06 +0530 Subject: [PATCH 3/5] fix: allow admins to edit question sets without enforcing future deadline date --- admin-client/src/pages/admin/QuestionSetsTab.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin-client/src/pages/admin/QuestionSetsTab.jsx b/admin-client/src/pages/admin/QuestionSetsTab.jsx index ab08832..bf2dbd5 100644 --- a/admin-client/src/pages/admin/QuestionSetsTab.jsx +++ b/admin-client/src/pages/admin/QuestionSetsTab.jsx @@ -570,7 +570,7 @@ const QuestionSetsTab = () => {
setForm({...form,weekNumber:Number(e.target.value)})}/>
-
setForm({...form,deadline:e.target.value})}/>
+
setForm({...form,deadline:e.target.value})}/>
From d4a5d5662943382a0bcfb0d4fa4c15e845384880 Mon Sep 17 00:00:00 2001 From: nirakarpatel Date: Sat, 18 Jul 2026 13:59:18 +0530 Subject: [PATCH 4/5] feat(admin): enforce solution-only edits for past-deadline question sets --- .../src/pages/admin/QuestionSetsTab.jsx | 2 +- .../challenges/questionSet.controller.js | 41 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/admin-client/src/pages/admin/QuestionSetsTab.jsx b/admin-client/src/pages/admin/QuestionSetsTab.jsx index bf2dbd5..47b4f05 100644 --- a/admin-client/src/pages/admin/QuestionSetsTab.jsx +++ b/admin-client/src/pages/admin/QuestionSetsTab.jsx @@ -303,7 +303,7 @@ const QuestionSetsTab = () => { const updateSetMutation = useMutation({ mutationFn:async({id,body})=>(await api.put(`/api/sets/${id}`,body)).data, - onSuccess:()=>{toast.success('Question Set updated!');qc.invalidateQueries({ queryKey: ['admin-question-sets'] });qc.invalidateQueries({ queryKey: ['admin-manage-challenges'] });setView('list');setEditingSetId(null);setForm(blankForm())}, + onSuccess:(res)=>{toast.success(res.message||'Question Set updated!');qc.invalidateQueries({ queryKey: ['admin-question-sets'] });qc.invalidateQueries({ queryKey: ['admin-manage-challenges'] });setView('list');setEditingSetId(null);setForm(blankForm())}, onError:(e)=>toast.error(e.response?.data?.message||'Failed') }); diff --git a/server/src/features/challenges/questionSet.controller.js b/server/src/features/challenges/questionSet.controller.js index 7c8c478..505f369 100644 --- a/server/src/features/challenges/questionSet.controller.js +++ b/server/src/features/challenges/questionSet.controller.js @@ -136,6 +136,47 @@ const updateQuestionSet = async (req, res, next) => { throw new Error('Question Set not found'); } + const now = new Date(); + const currentDeadline = new Date(set.deadline); + currentDeadline.setUTCHours(23, 59, 59, 999); + const isPastDeadline = currentDeadline < now; + + if (isPastDeadline) { + // Only allow updating solutions + if (Array.isArray(req.body.questions)) { + const existingQs = set.questions; + for (const incomingQ of req.body.questions) { + if (!incomingQ.title) continue; + const match = existingQs.find(q => q.title.toLowerCase() === incomingQ.title.toLowerCase()); + if (match) { + match.solutions = incomingQ.solutions || match.solutions; + } + } + set.markModified('questions'); + await set.save(); + + // Update standalone challenges for solutions ONLY + const existingChallenges = await Challenge.find({ questionSetId: set._id }); + for (const existingC of existingChallenges) { + const match = req.body.questions.find(q => q.title && q.title.toLowerCase() === existingC.title.toLowerCase()); + if (match) { + existingC.solutions = match.solutions || existingC.solutions; + await existingC.save(); + } + } + } + + await logAudit({ + action: 'questionset.update_solutions_only', + actorId: req.user.id, + targetType: 'questionset', + targetId: set._id, + metadata: { title: set.title, note: 'Past deadline, only solutions updated' }, + }); + + return sendSuccess(res, { data: set, message: 'Solutions saved! Other changes ignored (past deadline).' }); + } + if (req.body.title) { req.body.title = capitalizeTitle(req.body.title); } From 97752d082d0b39a0d60fc238f6b5a87ee838c822 Mon Sep 17 00:00:00 2001 From: nirakarpatel Date: Sat, 18 Jul 2026 14:01:09 +0530 Subject: [PATCH 5/5] feat: disable non-solution fields for past-deadline question sets --- .../src/pages/admin/QuestionSetsTab.jsx | 68 ++++++++++--------- .../challenges/questionSet.controller.js | 41 ----------- 2 files changed, 37 insertions(+), 72 deletions(-) diff --git a/admin-client/src/pages/admin/QuestionSetsTab.jsx b/admin-client/src/pages/admin/QuestionSetsTab.jsx index 47b4f05..133d5db 100644 --- a/admin-client/src/pages/admin/QuestionSetsTab.jsx +++ b/admin-client/src/pages/admin/QuestionSetsTab.jsx @@ -291,6 +291,9 @@ const QuestionSetsTab = () => { const [solutionLangByKey, setSolutionLangByKey] = useState({}); const [previewQuestion, setPreviewQuestion] = useState(null); + const todayStr = new Date().toISOString().split('T')[0]; + const isPastDeadline = editingSetId && form.deadline && form.deadline < todayStr; + const blankForm = ()=>({title:'',weekNumber:1,deadline:'',targetLevel:'Both',questions:[{...initialQuestionState}]}); const setsQuery = useQuery({queryKey:['admin-question-sets'],queryFn:async()=>{try{const r=await api.get('/api/sets');return r.data.data||[]}catch{return[]}}}); @@ -303,7 +306,7 @@ const QuestionSetsTab = () => { const updateSetMutation = useMutation({ mutationFn:async({id,body})=>(await api.put(`/api/sets/${id}`,body)).data, - onSuccess:(res)=>{toast.success(res.message||'Question Set updated!');qc.invalidateQueries({ queryKey: ['admin-question-sets'] });qc.invalidateQueries({ queryKey: ['admin-manage-challenges'] });setView('list');setEditingSetId(null);setForm(blankForm())}, + onSuccess:()=>{toast.success('Question Set updated!');qc.invalidateQueries({ queryKey: ['admin-question-sets'] });qc.invalidateQueries({ queryKey: ['admin-manage-challenges'] });setView('list');setEditingSetId(null);setForm(blankForm())}, onError:(e)=>toast.error(e.response?.data?.message||'Failed') }); @@ -496,38 +499,38 @@ const QuestionSetsTab = () => { ); }; - const HintsEditor = ({hints,onChange}) => { + const HintsEditor = ({hints,onChange,disabled}) => { const list = hints && hints.length ? hints : ['']; return (
- + {!disabled && }
{list.map((hint,hi)=>(
- {const nh=[...list];nh[hi]=e.target.value;onChange(nh)}}/> - {list.length>1&&} + {const nh=[...list];nh[hi]=e.target.value;onChange(nh)}}/> + {list.length>1&&!disabled&&}
))}
); }; - const TestCaseEditor = ({cases,onChange}) => ( + const TestCaseEditor = ({cases,onChange,disabled}) => (
- + {!disabled && }
{(!cases||cases.length===0)?

No test cases — click Add Case.

:(
{cases.map((tc,i)=>( -
-

Label

{const t=[...cases];t[i]={...t[i],label:e.target.value};onChange(t)}}/>
-

Args (JSON array)

{const t=[...cases];t[i]={...t[i],args:e.target.value};onChange(t)}}/>
-

Expected

{const t=[...cases];t[i]={...t[i],expected:e.target.value};onChange(t)}}/>
- +
+

Label

{const t=[...cases];t[i]={...t[i],label:e.target.value};onChange(t)}}/>
+

Args (JSON array)

{const t=[...cases];t[i]={...t[i],args:e.target.value};onChange(t)}}/>
+

Expected

{const t=[...cases];t[i]={...t[i],expected:e.target.value};onChange(t)}}/>
+ {!disabled && }
))}
@@ -559,25 +562,28 @@ const QuestionSetsTab = () => {
-

Set Details

-
-
setForm({...form,title:e.target.value})}/>
+
+

Set Details

+ {isPastDeadline && Past Deadline - Only solutions can be updated} +
+
+
setForm({...form,title:e.target.value})}/>
- {['Beginner','Intermediate','Both'].map(lvl=>())} + {['Beginner','Intermediate','Both'].map(lvl=>())}
-
setForm({...form,weekNumber:Number(e.target.value)})}/>
+
setForm({...form,weekNumber:Number(e.target.value)})}/>
-
setForm({...form,deadline:e.target.value})}/>
+
setForm({...form,deadline:e.target.value})}/>

Questions ({form.questions.length}/5)

- {form.questions.length<5&&()} + {form.questions.length<5&&!isPastDeadline&&()}
@@ -589,26 +595,26 @@ const QuestionSetsTab = () => {

{i+1} Question Config

- {form.questions.length>1&&()} + {form.questions.length>1&&!isPastDeadline&&()}
-
updateQuestion(i,'leetcodeSlug',e.target.value)}/>
-
-
updateQuestion(i,'title',e.target.value)}/>
-
-
updateQuestion(i,'points',Number(e.target.value))}/>
-
updateQuestion(i,'category',e.target.value)}/>
-
updateQuestion(i,'orderIndependent',e.target.checked)}/>
-