feat: Improve Assignment UI and File Upload Responsiveness

This commit is contained in:
swve 2025-02-26 17:39:37 +01:00
parent df4cc587ba
commit 85cbb100fe
3 changed files with 133 additions and 94 deletions

View file

@ -44,6 +44,12 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
});
const handleFileChange = async (event: any) => {
// Check if user is authenticated
if (!access_token) {
setError('Authentication required. Please sign in to upload files.');
return;
}
const file = event.target.files[0]
setLocalUploadFile(file)
@ -69,21 +75,30 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
setIsLoading(false)
setError('')
}
}
async function getAssignmentTaskSubmissionFromUserUI() {
if (!access_token) {
// Silently fail if not authenticated
return;
}
if (assignmentTaskUUID) {
const res = await getAssignmentTaskSubmissionsMe(assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) {
setUserSubmissions(res.data.task_submission);
setInitialUserSubmissions(res.data.task_submission);
}
}
}
const submitFC = async () => {
// Check if user is authenticated
if (!access_token) {
toast.error('Authentication required. Please sign in to submit your task.');
return;
}
// Save the quiz to the server
const values = {
task_submission: userSubmissions,
@ -105,13 +120,17 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
};
async function getAssignmentTaskUI() {
if (!access_token) {
// Silently fail if not authenticated
return;
}
if (assignmentTaskUUID) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
if (res.success) {
setAssignmentTask(res.data);
setAssignmentTaskOutsideProvider(res.data);
}
}
}
@ -129,6 +148,11 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
/* GRADING VIEW CODE */
const [userSubmissionObject, setUserSubmissionObject] = useState<any>(null);
async function getAssignmentTaskSubmissionFromIdentifiedUserUI() {
if (!access_token) {
// Silently fail if not authenticated
return;
}
if (assignmentTaskUUID && user_id) {
const res = await getAssignmentTaskSubmissionsUser(assignmentTaskUUID, user_id, assignment.assignment_object.assignment_uuid, access_token);
if (res.success) {
@ -136,7 +160,6 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
setUserSubmissionObject(res.data);
setInitialUserSubmissions(res.data.task_submission);
}
}
}
@ -186,31 +209,29 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
return (
<AssignmentBoxUI submitFC={submitFC} showSavingDisclaimer={showSavingDisclaimer} view={view} gradeCustomFC={gradeCustomFC} currentPoints={userSubmissionObject?.grade} maxPoints={assignmentTaskOutsideProvider?.max_grade_value} type="file">
{view === 'teacher' && (
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Info size={20} />
<div className='flex flex-col sm:flex-row py-5 sm:py-6 text-xs sm:text-sm justify-center mx-auto space-y-2 sm:space-y-0 sm:space-x-3 text-slate-600 px-4 sm:px-2 text-center sm:text-left bg-slate-50 rounded-lg border border-slate-100'>
<Info size={18} className="mx-auto sm:mx-0 text-slate-500" />
<p>User will be able to submit a file for this task, you'll be able to review it in the Submissions Tab</p>
</div>
)}
{view === 'custom-grading' && (
<div className='flex flex-col space-y-1'>
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Download size={20} />
<div className='flex flex-col space-y-4 w-full px-2 sm:px-0'>
<div className='flex flex-col sm:flex-row py-5 sm:py-6 text-xs sm:text-sm justify-center mx-auto space-y-2 sm:space-y-0 sm:space-x-3 text-slate-600 px-4 sm:px-2 text-center sm:text-left bg-slate-50 rounded-lg border border-slate-100'>
<Download size={18} className="mx-auto sm:mx-0 text-slate-500" />
<p>Please download the file and grade it manually, then input the grade above</p>
</div>
{userSubmissions.fileUUID && !isLoading && assignmentTaskUUID && (
<Link
href={getTaskFileSubmissionDir(org?.org_uuid, assignment.course_object.course_uuid, assignment.activity_object.activity_uuid, assignment.assignment_object.assignment_uuid, assignmentTaskUUID, userSubmissions.fileUUID)}
target='_blank'
className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
<Cloud size={15} />
className='flex flex-col rounded-lg bg-white text-gray-500 shadow-sm hover:shadow-md transition-shadow border border-gray-100 px-4 sm:px-5 py-4 space-y-1 items-center relative w-full sm:w-auto mx-auto'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-emerald-500 rounded-full p-1.5 text-white flex justify-center items-center shadow-sm'>
<Cloud size={14} />
</div>
<div
className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
<div className='flex space-x-2 mt-2 items-center'>
<File size={18} className="text-emerald-500" />
<div className='font-medium text-xs sm:text-sm uppercase break-all'>
{`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`}
</div>
</div>
@ -220,66 +241,72 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
)}
{view === 'student' && (
<>
<div className="w-auto bg-white rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow">
<div className="flex flex-col justify-center items-center h-full">
<div className="flex flex-col justify-center items-center">
<div className="flex flex-col justify-center items-center">
<div className="w-full bg-white rounded-lg border border-gray-100 min-h-[200px] shadow-sm px-4 sm:px-6 py-5 sm:py-6">
<div className="flex flex-col justify-center items-center h-full w-full">
<div className="flex flex-col justify-center items-center w-full max-w-full">
<div className="flex flex-col justify-center items-center w-full">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
<div className="text-sm font-semibold">{error}</div>
<div className="flex justify-center bg-red-50 border border-red-100 rounded-md text-red-600 space-x-2 items-center p-3 transition-all shadow-sm w-full sm:w-auto mb-4">
<div className="text-xs sm:text-sm font-medium">{error}</div>
</div>
)}
</div>
{localUploadFile && !isLoading && (
<div className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
<Cloud size={15} />
<div className='flex flex-col rounded-lg bg-white text-gray-500 shadow-sm border border-gray-100 px-4 sm:px-5 py-4 space-y-1 items-center relative w-full sm:w-auto mt-3'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-emerald-500 rounded-full p-1.5 text-white flex justify-center items-center shadow-sm'>
<Cloud size={14} />
</div>
<div className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{localUploadFile.name}
<div className='flex space-x-2 mt-2 items-center'>
<File size={18} className="text-emerald-500" />
<div className='font-medium text-xs sm:text-sm uppercase break-all'>
{localUploadFile.name.length > 20
? `${localUploadFile.name.slice(0, 10)}...${localUploadFile.name.slice(-10)}`
: localUploadFile.name}
</div>
</div>
</div>
)}
{userSubmissions.fileUUID && !isLoading && !localUploadFile && (
<div className='flex flex-col rounded-lg bg-white text-gray-400 shadow-lg nice-shadow px-5 py-3 space-y-1 items-center relative'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-green-500 rounded-full px-1.5 py-1.5 text-white flex justify-center items-center'>
<Cloud size={15} />
<div className='flex flex-col rounded-lg bg-white text-gray-500 shadow-sm border border-gray-100 px-4 sm:px-5 py-4 space-y-1 items-center relative w-full sm:w-auto mt-3'>
<div className='absolute top-0 right-0 transform translate-x-1/2 -translate-y-1/2 bg-emerald-500 rounded-full p-1.5 text-white flex justify-center items-center shadow-sm'>
<Cloud size={14} />
</div>
<div className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
<div className='flex space-x-2 mt-2 items-center'>
<File size={18} className="text-emerald-500" />
<div className='font-medium text-xs sm:text-sm uppercase break-all'>
{`${userSubmissions.fileUUID.slice(0, 8)}...${userSubmissions.fileUUID.slice(-4)}`}
</div>
</div>
</div>
)}
<div className='flex pt-4 font-semibold space-x-1.5 text-xs items-center text-gray-500 '>
<Info size={16} />
<p>Allowed formats : pdf, docx, mp4, jpg, jpeg, png, pptx, zip</p>
<div className='flex flex-col sm:flex-row pt-5 font-medium space-y-1 sm:space-y-0 sm:space-x-2 text-xs items-center text-slate-500 text-center sm:text-left bg-slate-50 rounded-lg px-3 py-2 mt-5 border border-slate-100 w-full sm:w-auto'>
<Info size={15} className="mx-auto sm:mx-0 text-slate-400" />
<p>Allowed formats: pdf, docx, mp4, jpg, jpeg, png, pptx, zip</p>
</div>
{isLoading ? (
<div className="flex justify-center items-center">
{!access_token ? (
<div className="flex justify-center items-center w-full mt-5">
<div className="flex justify-center bg-amber-50 border border-amber-100 rounded-md text-amber-600 space-x-2 items-center p-3 transition-all shadow-sm w-full sm:w-auto">
<Info size={15} className="text-amber-500" />
<div className="text-xs sm:text-sm font-medium">Please sign in to upload files</div>
</div>
</div>
) : isLoading ? (
<div className="flex justify-center items-center w-full mt-5">
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<div className="font-bold animate-pulse antialiased items-center bg-slate-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex">
<Loader size={16} className="mr-2" />
<div className="font-medium animate-pulse antialiased items-center bg-slate-100 text-slate-600 text-xs sm:text-sm rounded-md px-4 sm:px-5 py-2.5 flex">
<Loader size={15} className="mr-2" />
<span>Loading</span>
</div>
</div>
) : (
<div className="flex justify-center items-center">
<div className="flex justify-center items-center w-full mt-5">
<input
type="file"
id={"fileInput_" + assignmentTaskUUID}
@ -287,10 +314,10 @@ export default function TaskFileObject({ view, user_id, assignmentTaskUUID }: Ta
onChange={handleFileChange}
/>
<button
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
className="font-medium antialiased items-center text-white text-xs sm:text-sm rounded-md px-4 sm:px-5 py-2.5 flex bg-emerald-500 hover:bg-emerald-600 transition-colors shadow-sm"
onClick={() => document.getElementById("fileInput_" + assignmentTaskUUID)?.click()}
>
<UploadCloud size={16} className="mr-2" />
<UploadCloud size={15} className="mr-2" />
<span>Submit File</span>
</button>
</div>

View file

@ -1,6 +1,7 @@
import { useAssignmentSubmission } from '@components/Contexts/Assignments/AssignmentSubmissionContext'
import { BookPlus, BookUser, EllipsisVertical, FileUp, Forward, InfoIcon, ListTodo, Save } from 'lucide-react'
import React, { useEffect } from 'react'
import { useLHSession } from '@components/Contexts/LHSessionContext'
type AssignmentBoxProps = {
type: 'quiz' | 'file'
@ -13,21 +14,25 @@ type AssignmentBoxProps = {
gradeCustomFC?: (grade: number) => void
showSavingDisclaimer?: boolean
children: React.ReactNode
}
function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitFC, gradeFC, gradeCustomFC, showSavingDisclaimer, children }: AssignmentBoxProps) {
const [customGrade, setCustomGrade] = React.useState<number>(0)
const submission = useAssignmentSubmission() as any
const session = useLHSession() as any
useEffect(() => {
console.log(submission)
}
, [submission])
}, [submission])
// Check if user is authenticated
const isAuthenticated = session?.status === 'authenticated'
return (
<div className='flex flex-col px-6 py-4 nice-shadow rounded-md bg-slate-100/30'>
<div className='flex justify-between space-x-2 pb-2 text-slate-400 items-center'>
<div className='flex space-x-1 items-center'>
<div className='flex flex-col px-3 sm:px-6 py-4 nice-shadow rounded-md bg-slate-100/30'>
<div className='flex flex-col sm:flex-row sm:justify-between sm:space-x-2 pb-2 text-slate-400 sm:items-center'>
{/* Left side with type and badges */}
<div className='flex flex-wrap gap-2 items-center mb-2 sm:mb-0'>
<div className='text-lg font-semibold'>
{type === 'quiz' &&
<div className='flex space-x-1.5 items-center'>
@ -41,26 +46,27 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF
</div>}
</div>
<div className='flex items-center space-x-1'>
<EllipsisVertical size={15} />
</div>
{view === 'teacher' &&
<div className='flex bg-amber-200/20 text-xs rounded-full space-x-1 px-2 py-0.5 mx-auto font-bold outline items-center text-amber-600 outline-1 outline-amber-300/40'>
<div className='flex bg-amber-200/20 text-xs rounded-full space-x-1 px-2 py-0.5 font-bold outline items-center text-amber-600 outline-1 outline-amber-300/40'>
<BookUser size={12} />
<p>Teacher view</p>
</div>
}
{maxPoints &&
<div className='flex bg-emerald-200/20 text-xs rounded-full space-x-1 px-2 py-0.5 mx-auto font-bold outline items-center text-emerald-600 outline-1 outline-emerald-300/40'>
<div className='flex bg-emerald-200/20 text-xs rounded-full space-x-1 px-2 py-0.5 font-bold outline items-center text-emerald-600 outline-1 outline-emerald-300/40'>
<BookPlus size={12} />
<p>{maxPoints} points</p>
</div>
}
</div>
<div className='flex px-1 py-1 rounded-md items-center'>
{/* Right side with buttons and actions */}
<div className='flex flex-wrap gap-2 items-center'>
{showSavingDisclaimer &&
<div className='flex space-x-2 items-center font-semibold px-3 py-1 outline-dashed outline-red-200 text-red-400 mr-5 rounded-full'>
<div className='flex space-x-2 items-center font-semibold px-3 py-1 outline-dashed outline-red-200 text-red-400 sm:mr-5 rounded-full w-full sm:w-auto mb-2 sm:mb-0'>
<InfoIcon size={14} />
<p className='text-xs'>Don't forget to save your progress</p>
</div>
@ -76,11 +82,11 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF
</div>
}
{/* Student button */}
{view === 'student' && submission && submission.length <= 0 &&
{/* Student button - only show if authenticated */}
{view === 'student' && isAuthenticated && submission && submission.length <= 0 &&
<div
onClick={() => submitFC && submitFC()}
className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'>
className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center justify-center mx-auto w-full sm:w-auto bg-gradient-to-bl text-emerald-700 bg-emerald-300/20 hover:bg-emerald-300/10 hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-emerald-500/60'>
<Forward size={14} />
<p className='text-xs font-semibold'>Save your progress</p>
</div>
@ -89,11 +95,11 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF
{/* Grading button */}
{view === 'grading' &&
<div
className='flex px-0.5 py-0.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'>
<p className='font-semibold px-2 text-xs text-orange-700'>Current points : {currentPoints}</p>
className='flex flex-wrap sm:flex-nowrap w-full sm:w-auto px-0.5 py-0.5 cursor-pointer rounded-md gap-2 sm:space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'>
<p className='font-semibold px-2 text-xs text-orange-700'>Current points: {currentPoints}</p>
<div
onClick={() => gradeFC && gradeFC()}
className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2'>
className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2 ml-auto'>
<BookPlus size={14} />
<p className='text-xs font-semibold'>Grade</p>
</div>
@ -103,18 +109,23 @@ function AssignmentBoxUI({ type, view, currentPoints, maxPoints, saveFC, submitF
{/* CustomGrading button */}
{view === 'custom-grading' && maxPoints &&
<div
className='flex px-0.5 py-0.5 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'>
<p className='font-semibold px-2 text-xs text-orange-700'>Current points : {currentPoints}</p>
className='flex flex-wrap sm:flex-nowrap w-full sm:w-auto px-0.5 py-0.5 cursor-pointer rounded-md gap-2 sm:space-x-2 items-center bg-gradient-to-bl hover:outline-offset-4 active:outline-offset-1 linear transition-all outline-offset-2 outline-dashed outline-orange-500/60'>
<p className='font-semibold px-2 text-xs text-orange-700 w-full sm:w-auto'>Current points: {currentPoints}</p>
<div className='flex items-center gap-2 w-full sm:w-auto'>
<input
onChange={(e) => setCustomGrade(parseInt(e.target.value))}
placeholder={maxPoints.toString()} className='w-[100px] light-shadow text-sm py-0.5 outline outline-gray-200 rounded-lg px-2' type="number" />
placeholder={maxPoints.toString()}
className='w-full sm:w-[100px] light-shadow text-sm py-0.5 outline outline-gray-200 rounded-lg px-2'
type="number"
/>
<div
onClick={() => gradeCustomFC && gradeCustomFC(customGrade)}
className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2'>
className='bg-gradient-to-bl text-orange-700 bg-orange-300/20 hover:bg-orange-300/10 items-center flex rounded-md px-2 py-1 space-x-2 whitespace-nowrap'>
<BookPlus size={14} />
<p className='text-xs font-semibold'>Grade</p>
</div>
</div>
</div>
}
</div>
</div>

View file

@ -18,38 +18,39 @@ function AssignmentStudentActivity() {
return (
<div className='flex flex-col space-y-6'>
<div className='flex flex-row justify-center space-x-3 items-center '>
<div className='text-xs h-fit flex space-x-3 items-center '>
<div className='flex space-x-2 py-2 px-5 h-fit text-sm text-slate-700 bg-slate-100/5 rounded-full nice-shadow'>
<Backpack size={18} />
<div className='flex flex-col space-y-4 md:space-y-6'>
<div className='flex flex-col md:flex-row justify-center md:space-x-3 space-y-3 md:space-y-0 items-center'>
<div className='text-xs h-fit flex space-x-3 items-center'>
<div className='flex space-x-2 py-2 px-4 md:px-5 h-fit text-sm text-slate-700 bg-slate-100/5 rounded-full nice-shadow'>
<Backpack size={16} className="md:size-18" />
<p className='font-semibold'>Assignment</p>
</div>
</div>
<div>
<div className='flex space-x-2 items-center'>
<EllipsisVertical className='text-slate-400' size={18} />
<EllipsisVertical className='text-slate-400 hidden md:block' size={18} />
<div className='flex space-x-2 items-center'>
<div className='flex space-x-2 text-xs items-center text-slate-400'>
<div className='flex space-x-1 md:space-x-2 text-xs items-center text-slate-400'>
<Calendar size={14} />
<p className=' font-semibold'>Due Date</p>
<p className=' font-semibold'>{assignments?.assignment_object?.due_date}</p>
<p className='font-semibold'>Due Date</p>
<p className='font-semibold'>{assignments?.assignment_object?.due_date}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div className='w-full rounded-full bg-slate-500/5 nice-shadow h-[2px]'></div>
{assignments && assignments?.assignment_tasks?.sort((a: any, b: any) => a.id - b.id).map((task: any, index: number) => {
return (
<div className='flex flex-col space-y-2' key={task.assignment_task_uuid}>
<div className='flex justify-between py-2'>
<div className='flex space-x-2 font-semibold text-slate-800'>
<div className='flex flex-col md:flex-row md:justify-between py-2 space-y-2 md:space-y-0'>
<div className='flex flex-wrap space-x-2 font-semibold text-slate-800'>
<p>Task {index + 1} : </p>
<p className='text-slate-500'>{task.description}</p>
<p className='text-slate-500 break-words'>{task.description}</p>
</div>
<div className='flex space-x-2'>
<div className='flex flex-wrap gap-2'>
<div
onClick={() => alert(task.hint)}
className='px-3 py-1 flex items-center nice-shadow bg-amber-50/40 text-amber-900 rounded-full space-x-2 cursor-pointer'>
@ -67,9 +68,9 @@ function AssignmentStudentActivity() {
)}
target='_blank'
download={true}
className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-2 cursor-pointer'>
className='px-3 py-1 flex items-center nice-shadow bg-cyan-50/40 text-cyan-900 rounded-full space-x-1 md:space-x-2 cursor-pointer'>
<Download size={13} />
<div className='flex items-center space-x-2'>
<div className='flex items-center space-x-1 md:space-x-2'>
{task.reference_file && (
<span className='relative'>
<span className='absolute right-0 top-0 block h-2 w-2 rounded-full ring-2 ring-white bg-green-400'></span>
@ -80,7 +81,7 @@ function AssignmentStudentActivity() {
</Link>
</div>
</div>
<div>
<div className='w-full'>
{task.assignment_type === 'QUIZ' && <TaskQuizObject key={task.assignment_task_uuid} view='student' assignmentTaskUUID={task.assignment_task_uuid} />}
{task.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject key={task.assignment_task_uuid} view='student' assignmentTaskUUID={task.assignment_task_uuid} />}
</div>