Add SECURITY_TRIAGE.md for vulnerability assessment and remediation plan
- Document current state of vulnerabilities in Rust and frontend dependencies - Outline active vulnerabilities and their remediation status - Include notes on resolved issues and remaining bugs - Define a remediation plan with prioritized actions
This commit is contained in:
Generated
+686
-401
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
||||
"isomorphic-dompurify": "^3.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.395.0",
|
||||
"mermaid": "^9.1.7",
|
||||
"mermaid": "^11.14.0",
|
||||
"next": "^14.2.35",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
|
||||
@@ -145,6 +145,7 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les
|
||||
}
|
||||
return [...prev, res];
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`Failed to submit score for block ${blockId}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,262 +323,3 @@ export default function PeerReviewPlayer({ courseId, lessonId, block }: PeerRevi
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
interface PeerReviewPlayerProps {
|
||||
courseId: string;
|
||||
lessonId: string;
|
||||
block: Block;
|
||||
}
|
||||
|
||||
export default function PeerReviewPlayer({ courseId, lessonId, block }: PeerReviewPlayerProps) {
|
||||
const [view, setView] = useState<'submit' | 'dashboard' | 'reviewing'>('submit');
|
||||
const [submissionContent, setSubmissionContent] = useState("");
|
||||
const [mySubmission, setMySubmission] = useState<CourseSubmission | null>(null);
|
||||
const [peerAssignment, setPeerAssignment] = useState<CourseSubmission | null>(null);
|
||||
const [feedbackReceived, setFeedbackReceived] = useState<PeerReview[]>([]);
|
||||
|
||||
// Review form state
|
||||
const [reviewScore, setReviewScore] = useState(80);
|
||||
const [reviewFeedback, setReviewFeedback] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [message, setMessage] = useState("");
|
||||
|
||||
// Initial check - do we have a submission?
|
||||
// We don't have an endpoint for "get my submission" directly in the list I made,
|
||||
// but I can infer it or try to fetch feedback.
|
||||
// Actually, I missed adding "get my submission" endpoint.
|
||||
// Additionaly `getPeerReviewAssignment` excludes my own.
|
||||
// For now, let's assume if I can fetch feedback, I have submitted.
|
||||
// Or I can add a check endpoint.
|
||||
// Let's try to fetch feedback first.
|
||||
|
||||
useEffect(() => {
|
||||
const checkStatus = async () => {
|
||||
// For simplify, we just check feedback. If error or empty, maybe no submission?
|
||||
// Actually, `getMySubmissionFeedback` returns array.
|
||||
try {
|
||||
const reviews = await lmsApi.getMySubmissionFeedback(courseId, lessonId);
|
||||
setFeedbackReceived(reviews);
|
||||
// If we get reviews (or even empty array), it implies we might have submitted?
|
||||
// Not necessarily.
|
||||
// I should have added `getMySubmission`.
|
||||
// But for now, let's rely on local storage or just show "Submit" if no local state.
|
||||
// Ideally backend check.
|
||||
} catch (err) {
|
||||
// Error might mean not authorized or something.
|
||||
}
|
||||
};
|
||||
checkStatus();
|
||||
}, [courseId, lessonId]);
|
||||
|
||||
// Workaround: We will use a "saved" state in localStorage for "submitted" to avoid needing another endpoint right now,
|
||||
// or better: just try to submit. If it says "already submitted", handle it?
|
||||
// The backend `submit_assignment` UPDATES if exists. So it's safe to show form with previous content if we had it.
|
||||
// But we don't have "get my content".
|
||||
|
||||
// Let's add a "View my submission" button if I assume I submitted?
|
||||
// Maybe just show the dashboard if I have feedback.
|
||||
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const sub = await lmsApi.submitAssignment(courseId, lessonId, submissionContent);
|
||||
setMySubmission(sub);
|
||||
setView('dashboard');
|
||||
setMessage("Submission saved successfully!");
|
||||
} catch (err: any) {
|
||||
setMessage("Failed to submit: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartReview = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const assignment = await lmsApi.getPeerReviewAssignment(courseId, lessonId);
|
||||
if (!assignment) {
|
||||
setMessage("No assignments available for review at the moment. Please try again later.");
|
||||
} else {
|
||||
setPeerAssignment(assignment);
|
||||
setView('reviewing');
|
||||
setMessage("");
|
||||
}
|
||||
} catch (err: any) {
|
||||
setMessage("Error fetching assignment: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitReview = async () => {
|
||||
if (!peerAssignment) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
await lmsApi.submitPeerReview(courseId, lessonId, peerAssignment.id, reviewScore, reviewFeedback);
|
||||
setMessage("Review submitted successfully! Thank you.");
|
||||
setPeerAssignment(null);
|
||||
setReviewFeedback("");
|
||||
setView('dashboard');
|
||||
} catch (err: any) {
|
||||
setMessage("Failed to submit review: " + err.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (view === 'reviewing' && peerAssignment) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<button onClick={() => setView('dashboard')} className="text-sm text-gray-400 hover:text-white mb-4">
|
||||
← Back to Dashboard
|
||||
</button>
|
||||
<div className="p-6 bg-white/5 border border-white/10 rounded-2xl space-y-4">
|
||||
<h3 className="font-bold text-lg text-purple-400">Reviewing Peer Submission</h3>
|
||||
<div className="p-4 bg-black/30 rounded-xl text-gray-300 whitespace-pre-wrap">
|
||||
{peerAssignment.content}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-white/5 border border-white/10 rounded-2xl space-y-6">
|
||||
<h4 className="font-bold text-white">Your Feedback</h4>
|
||||
|
||||
{block.reviewCriteria && (
|
||||
<div className="text-sm text-gray-400 bg-blue-500/10 p-4 rounded-xl">
|
||||
<strong>Criteria:</strong> {block.reviewCriteria}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase text-gray-500 mb-2">Score (0-100)</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="100"
|
||||
value={reviewScore}
|
||||
onChange={(e) => setReviewScore(parseInt(e.target.value))}
|
||||
className="bg-black/20 border border-white/10 rounded-lg px-4 py-2 text-white w-24"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold uppercase text-gray-500 mb-2">Comments</label>
|
||||
<textarea
|
||||
value={reviewFeedback}
|
||||
onChange={(e) => setReviewFeedback(e.target.value)}
|
||||
className="w-full bg-black/20 border border-white/10 rounded-xl p-4 min-h-[120px] text-white focus:outline-none focus:border-purple-500"
|
||||
placeholder="Provide constructive feedback..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSubmitReview}
|
||||
disabled={loading || !reviewFeedback}
|
||||
className="btn-primary w-full py-3 font-bold uppercase tracking-widest text-xs rounded-xl bg-purple-600 hover:bg-purple-700 transition-colors disabled:opacity-50"
|
||||
>
|
||||
{loading ? "Submitting..." : "Submit Review"}
|
||||
</button>
|
||||
{message && <p className="text-center text-sm text-red-400">{message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (view === 'dashboard') {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="p-6 bg-green-500/10 border border-green-500/20 rounded-2xl flex items-center gap-4">
|
||||
<div className="text-2xl">✅</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-green-400">Work Submitted</h3>
|
||||
<p className="text-xs text-green-300/70">You can update your submission below or start reviewing peers.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-bold text-sm uppercase text-gray-500 tracking-widest">Actions</h4>
|
||||
<button
|
||||
onClick={handleStartReview}
|
||||
className="w-full p-6 bg-white/5 border border-white/10 rounded-2xl hover:bg-purple-500/10 hover:border-purple-500/30 transition-all text-left group"
|
||||
>
|
||||
<span className="text-2xl mb-2 block group-hover:scale-110 transition-transform">👀</span>
|
||||
<div className="font-bold text-purple-400">Review a Peer</div>
|
||||
<div className="text-xs text-gray-500 mt-1">Earn credit by reviewing other students' work.</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setView('submit')}
|
||||
className="w-full p-6 bg-white/5 border border-white/10 rounded-2xl hover:bg-blue-500/10 hover:border-blue-500/30 transition-all text-left"
|
||||
>
|
||||
<span className="text-2xl mb-2 block">📝</span>
|
||||
<div className="font-bold text-blue-400">Edit My Submission</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-bold text-sm uppercase text-gray-500 tracking-widest">Feedback Received</h4>
|
||||
{feedbackReceived.length === 0 ? (
|
||||
<div className="p-6 bg-white/5 border border-white/10 rounded-2xl text-center text-gray-500 italic text-sm">
|
||||
No reviews received yet. Check back later!
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{feedbackReceived.map(review => (
|
||||
<div key={review.id} className="p-4 bg-white/5 border border-white/10 rounded-xl space-y-2">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-xs font-bold text-gray-500">Peer Review</span>
|
||||
<span className="text-sm font-bold text-yellow-400">{review.score}/100</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-300">{review.feedback}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{message && <p className="text-center text-sm text-gray-400">{message}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Default: Submit View
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-xl font-bold flex items-center gap-2">
|
||||
<span className="text-purple-400">👥</span> {block.title || "Peer Assessment"}
|
||||
</h3>
|
||||
<div className="p-6 bg-white/5 border border-white/10 rounded-2xl whitespace-pre-wrap text-gray-300">
|
||||
{block.prompt || "Please submit your work below."}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<textarea
|
||||
value={submissionContent}
|
||||
onChange={(e) => setSubmissionContent(e.target.value)}
|
||||
className="w-full bg-black/20 border border-white/10 rounded-2xl p-6 min-h-[200px] text-white focus:outline-none focus:border-purple-500 transition-all"
|
||||
placeholder="Type your submission here or paste a link..."
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
{feedbackReceived.length > 0 && (
|
||||
<button onClick={() => setView('dashboard')} className="text-sm text-gray-500 hover:text-white">
|
||||
Cancel
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !submissionContent}
|
||||
className="btn-primary px-8 py-3 rounded-xl font-bold uppercase tracking-widest text-xs bg-blue-600 hover:bg-blue-700 transition-colors disabled:opacity-50 ml-auto"
|
||||
>
|
||||
{loading ? "Submitting..." : (mySubmission ? "Update Submission" : "Submit Assignment")}
|
||||
</button>
|
||||
</div>
|
||||
{message && <p className="text-center text-sm text-gray-400">{message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Generated
+745
-402
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
||||
"framer-motion": "^11.2.10",
|
||||
"isomorphic-dompurify": "^3.10.0",
|
||||
"lucide-react": "^0.395.0",
|
||||
"mermaid": "^9.1.7",
|
||||
"mermaid": "^11.14.0",
|
||||
"next": "^14.2.35",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
|
||||
@@ -588,271 +588,3 @@ export default function PeerReviewDashboard() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default function PeerReviewDashboard() {
|
||||
const { id } = useParams() as { id: string };
|
||||
const router = useRouter();
|
||||
const [course, setCourse] = useState<Course | null>(null);
|
||||
const [lessons, setLessons] = useState<Lesson[]>([]);
|
||||
const [selectedLessonId, setSelectedLessonId] = useState<string | null>(null);
|
||||
const [submissions, setSubmissions] = useState<SubmissionWithReviews[]>([]);
|
||||
const [selectedSubmissionId, setSelectedSubmissionId] = useState<string | null>(null);
|
||||
const [reviews, setReviews] = useState<PeerReview[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [submissionsLoading, setSubmissionsLoading] = useState(false);
|
||||
const [reviewsLoading, setReviewsLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const loadInitialData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const courseData = await cmsApi.getCourseWithFullOutline(id);
|
||||
setCourse(courseData);
|
||||
|
||||
const peerReviewLessons: Lesson[] = [];
|
||||
courseData.modules?.forEach(m => {
|
||||
m.lessons.forEach(l => {
|
||||
const hasPeerReview = l.metadata?.blocks?.some((b: any) => b.type === 'peer-review');
|
||||
if (hasPeerReview) {
|
||||
peerReviewLessons.push(l);
|
||||
}
|
||||
});
|
||||
});
|
||||
setLessons(peerReviewLessons);
|
||||
|
||||
if (peerReviewLessons.length > 0) {
|
||||
setSelectedLessonId(peerReviewLessons[0].id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error loading course data:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
loadInitialData();
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedLessonId) return;
|
||||
|
||||
const loadSubmissions = async () => {
|
||||
try {
|
||||
setSubmissionsLoading(true);
|
||||
const data = await lmsApi.listLessonSubmissions(id, selectedLessonId);
|
||||
setSubmissions(data);
|
||||
} catch (error) {
|
||||
console.error("Error loading submissions:", error);
|
||||
} finally {
|
||||
setSubmissionsLoading(false);
|
||||
}
|
||||
};
|
||||
loadSubmissions();
|
||||
}, [id, selectedLessonId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedSubmissionId) {
|
||||
setReviews([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const loadReviews = async () => {
|
||||
try {
|
||||
setReviewsLoading(true);
|
||||
const data = await lmsApi.getSubmissionReviews(selectedSubmissionId);
|
||||
setReviews(data);
|
||||
} catch (error) {
|
||||
console.error("Error loading reviews:", error);
|
||||
} finally {
|
||||
setReviewsLoading(false);
|
||||
}
|
||||
};
|
||||
loadReviews();
|
||||
}, [selectedSubmissionId]);
|
||||
|
||||
const filteredSubmissions = submissions.filter(s =>
|
||||
s.full_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
s.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent flex items-center justify-center">
|
||||
<Loader2 className="w-10 h-10 text-blue-500 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-transparent text-gray-900 dark:text-white p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<button onClick={() => router.back()} className="p-2 hover:bg-white/10 rounded-full transition-colors">
|
||||
<ArrowLeft className="w-6 h-6" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-4xl font-black bg-gradient-to-r from-purple-600 to-blue-600 bg-clip-text text-transparent uppercase tracking-tighter">
|
||||
Peer Assessment
|
||||
</h1>
|
||||
<p className="text-slate-500 dark:text-gray-400 mt-1 font-medium">Monitor and manage student peer feedback loops</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CourseEditorLayout activeTab="peer-reviews">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-10">
|
||||
{/* Lessons List */}
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-[10px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-[0.2em] px-4">Learning Activities</h3>
|
||||
<div className="space-y-2">
|
||||
{lessons.length === 0 ? (
|
||||
<div className="p-6 bg-slate-50 dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl text-sm text-slate-400 italic">
|
||||
No peer review activities found.
|
||||
</div>
|
||||
) : (
|
||||
lessons.map(lesson => (
|
||||
<button
|
||||
key={lesson.id}
|
||||
onClick={() => {
|
||||
setSelectedLessonId(lesson.id);
|
||||
setSelectedSubmissionId(null);
|
||||
}}
|
||||
className={`w-full text-left p-5 rounded-[1.5rem] border transition-all active:scale-95 ${selectedLessonId === lesson.id
|
||||
? "bg-purple-50 dark:bg-purple-500/10 border-purple-200 dark:border-purple-500/50 text-purple-600 dark:text-purple-400 shadow-md shadow-purple-500/5 font-black uppercase tracking-tight"
|
||||
: "bg-white dark:bg-white/5 border-slate-200 dark:border-white/10 text-slate-500 dark:text-gray-400 hover:border-purple-500/30 font-bold"
|
||||
}`}
|
||||
>
|
||||
<div className="truncate text-sm">{lesson.title}</div>
|
||||
<div className="text-[9px] uppercase font-black tracking-widest opacity-60 mt-1">Requirement</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submissions List */}
|
||||
<div className="lg:col-span-3 space-y-8">
|
||||
<div className="bg-white dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[2.5rem] p-10 shadow-sm">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-10">
|
||||
<h2 className="text-2xl font-black flex items-center gap-4 uppercase tracking-tight text-slate-900 dark:text-white">
|
||||
<div className="w-12 h-12 rounded-2xl bg-blue-50 dark:bg-blue-500/10 border border-blue-100 dark:border-blue-500/20 flex items-center justify-center text-blue-600 dark:text-blue-400 shadow-sm">
|
||||
<Users size={24} />
|
||||
</div>
|
||||
Submissions
|
||||
</h2>
|
||||
<div className="relative w-full md:w-80 group">
|
||||
<Search className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-300 dark:text-gray-500 w-5 h-5 group-focus-within:text-blue-500 transition-colors" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search student or email..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full bg-slate-50 dark:bg-black/20 border border-slate-200 dark:border-white/10 rounded-[1.25rem] py-4 pl-12 pr-6 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/30 text-slate-900 dark:text-white transition-all shadow-inner"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{submissionsLoading ? (
|
||||
<div className="flex flex-col items-center justify-center py-32 space-y-4">
|
||||
<Loader2 className="w-12 h-12 text-blue-500 animate-spin" />
|
||||
<span className="text-xs font-black uppercase tracking-widest text-slate-400">Loading records...</span>
|
||||
</div>
|
||||
) : filteredSubmissions.length === 0 ? (
|
||||
<div className="text-center py-32 bg-slate-50 dark:bg-black/20 rounded-3xl border border-dashed border-slate-200 dark:border-white/10">
|
||||
<div className="w-20 h-20 bg-white dark:bg-white/5 rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-sm">
|
||||
<MessageSquare className="w-10 h-10 text-slate-300 dark:text-gray-700" />
|
||||
</div>
|
||||
<p className="text-slate-500 dark:text-gray-500 font-bold uppercase tracking-tight">No submissions found for this activity.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 overflow-y-auto max-h-[700px] pr-4 custom-scrollbar">
|
||||
{filteredSubmissions.map(sub => (
|
||||
<div key={sub.id} className="group">
|
||||
<div
|
||||
onClick={() => setSelectedSubmissionId(selectedSubmissionId === sub.id ? null : sub.id)}
|
||||
className={`p-6 rounded-[1.5rem] border transition-all cursor-pointer shadow-sm active:scale-[0.99] ${selectedSubmissionId === sub.id
|
||||
? "bg-blue-50 dark:bg-blue-500/5 border-blue-200 dark:border-blue-500/30"
|
||||
: "bg-slate-50/50 dark:bg-white/[0.02] border-slate-200 dark:border-white/5 hover:bg-white dark:hover:bg-white/[0.05] hover:border-blue-500/30"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-blue-600 to-indigo-700 flex items-center justify-center font-black text-white text-lg shadow-lg shadow-blue-500/20">
|
||||
{sub.full_name.charAt(0)}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-black text-slate-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors uppercase tracking-tight text-sm">{sub.full_name}</div>
|
||||
<div className="text-[10px] font-bold text-slate-400 dark:text-gray-500 uppercase tracking-widest">{sub.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-10">
|
||||
<div className="text-right">
|
||||
<div className="text-lg font-black text-slate-900 dark:text-white flex items-center gap-2 justify-end">
|
||||
<Award className="w-5 h-5 text-yellow-500" />
|
||||
{sub.average_score !== null ? `${(sub.average_score).toFixed(1)}/10` : '—'}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-[0.2em]">Rating</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className={`text-lg font-black flex items-center gap-2 justify-end ${sub.review_count >= 2 ? 'text-green-600' : 'text-orange-500'}`}>
|
||||
<CheckCircle className="w-5 h-5" />
|
||||
{sub.review_count}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-[0.2em]">Feedback</div>
|
||||
</div>
|
||||
<div className="text-right hidden xl:block">
|
||||
<div className="text-sm font-bold text-slate-400 dark:text-gray-500 flex items-center gap-2 justify-end">
|
||||
<Clock className="w-4 h-4" />
|
||||
{new Date(sub.submitted_at).toLocaleDateString()}
|
||||
</div>
|
||||
<div className="text-[9px] text-slate-400 dark:text-gray-500 font-black uppercase tracking-[0.2em]">Delivery</div>
|
||||
</div>
|
||||
<ChevronRight className={`w-6 h-6 text-slate-300 transition-all ${selectedSubmissionId === sub.id ? 'rotate-90 text-blue-500' : ''}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reviews Detail Drawer-like expansion */}
|
||||
{selectedSubmissionId === sub.id && (
|
||||
<div className="mt-4 ml-8 p-10 bg-slate-50 dark:bg-black/40 border-l-4 border-blue-500 dark:border-blue-500/50 rounded-r-[2rem] space-y-8 animate-in slide-in-from-left-4 duration-300 shadow-inner">
|
||||
<h4 className="text-[10px] font-black uppercase tracking-[0.3em] text-blue-600 dark:text-blue-400 ml-1">Submission Feedback Details</h4>
|
||||
{reviewsLoading ? (
|
||||
<div className="flex py-10"><Loader2 className="w-8 h-8 animate-spin text-blue-500" /></div>
|
||||
) : reviews.length === 0 ? (
|
||||
<div className="p-8 bg-white dark:bg-white/5 border border-slate-200 dark:border-white/10 rounded-2xl text-center text-slate-400 italic">
|
||||
No peer reviews have been submitted for this user yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{reviews.map(review => (
|
||||
<div key={review.id} className="p-6 bg-white dark:bg-white/5 border border-slate-100 dark:border-white/10 rounded-2xl space-y-4 shadow-sm group/review hover:border-blue-500/30 transition-all">
|
||||
<div className="flex justify-between items-center pb-3 border-b border-slate-50 dark:border-white/5">
|
||||
<span className="text-[9px] font-black text-slate-400 dark:text-gray-500 uppercase tracking-widest">Peer Evaluator</span>
|
||||
<span className="px-3 py-1 bg-yellow-50 dark:bg-yellow-500/10 text-yellow-600 dark:text-yellow-500 text-sm font-black rounded-lg">
|
||||
{review.score}/10
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 dark:text-gray-300 leading-relaxed italic font-medium px-2">
|
||||
"{review.feedback}"
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CourseEditorLayout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user