feat: enable Tasks submissions

This commit is contained in:
swve 2024-07-19 19:56:49 +02:00
parent 6d7e521bba
commit bfb977ac5d
9 changed files with 330 additions and 110 deletions

View file

@ -2,13 +2,13 @@ import { useAssignments } from '@components/Contexts/Assignments/AssignmentConte
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'
import { getAssignmentTask, updateSubFile } from '@services/courses/assignments';
import { getAssignmentTask, getAssignmentTaskSubmissionsMe, handleAssignmentTaskSubmission, updateSubFile } from '@services/courses/assignments';
import { Cloud, File, Info, Loader, UploadCloud } from 'lucide-react'
import React, { useEffect } from 'react'
import React, { useEffect, useState } from 'react'
import toast from 'react-hot-toast';
type FileSchema = {
fileID: string;
fileUUID: string;
};
type TaskFileObjectProps = {
@ -26,6 +26,18 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
const assignment = useAssignments() as any;
/* TEACHER VIEW CODE */
/* TEACHER VIEW CODE */
/* STUDENT VIEW CODE */
const [showSavingDisclaimer, setShowSavingDisclaimer] = useState<boolean>(false);
const [userSubmissions, setUserSubmissions] = useState<FileSchema>({
fileUUID: '',
});
const [initialUserSubmissions, setInitialUserSubmissions] = useState<FileSchema>({
fileUUID: '',
});
const handleFileChange = async (event: any) => {
const file = event.target.files[0]
@ -37,19 +49,56 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
assignment.assignment_object.assignment_uuid,
access_token
)
assignmentTaskStateHook({ type: 'reload' })
// wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) {
setError(res.data.detail)
setIsLoading(false)
} else {
assignmentTaskStateHook({ type: 'reload' })
setUserSubmissions({
fileUUID: res.data.file_uuid,
})
setIsLoading(false)
setError('')
}
}
async function getAssignmentTaskSubmissionFromUserUI() {
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 () => {
// Save the quiz to the server
const values = {
task_submission: userSubmissions,
grade: 0,
task_submission_grade_feedback: '',
};
if (assignmentTaskUUID) {
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
assignmentTaskStateHook({
type: 'reload',
});
toast.success('Task saved successfully');
setShowSavingDisclaimer(false);
} else {
toast.error('Error saving task, please retry later.');
}
}
};
async function getAssignmentTaskUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
@ -60,13 +109,27 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
}
}
// Detect changes between initial and current submissions
useEffect(() => {
getAssignmentTaskUI()
if (userSubmissions.fileUUID !== initialUserSubmissions.fileUUID) {
setShowSavingDisclaimer(true);
} else {
setShowSavingDisclaimer(false);
}
}, [userSubmissions]);
/* STUDENT VIEW CODE */
useEffect(() => {
if (view === 'student') {
getAssignmentTaskUI()
getAssignmentTaskSubmissionFromUserUI()
}
}
, [assignmentTaskUUID])
return (
<AssignmentBoxUI view={view} type="file">
<AssignmentBoxUI submitFC={submitFC} showSavingDisclaimer={showSavingDisclaimer} view={view} type="file">
{view === 'teacher' && (
<div className='flex py-5 text-sm justify-center mx-auto space-x-2 text-slate-500'>
<Info size={20} />
@ -91,20 +154,35 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
<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>
<div className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{localUploadFile.name}
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{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>
<div className='flex space-x-2 mt-2'>
<File size={20} className='' />
<div className='font-semibold text-sm uppercase'>
{`${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, pptx</p>
</div>
<Info size={16} />
<p>Allowed formats : pdf, docx, mp4, jpg, jpeg, pptx</p>
</div>
{isLoading ? (
<div className="flex justify-center items-center">
<input
@ -122,13 +200,13 @@ export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObj
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
id={"fileInput_" + assignmentTaskUUID}
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
onClick={() => document.getElementById('fileInput')?.click()}
onClick={() => document.getElementById("fileInput_" + assignmentTaskUUID)?.click()}
>
<UploadCloud size={16} className="mr-2" />
<span>Submit File</span>

View file

@ -2,7 +2,7 @@ import { useAssignments } from '@components/Contexts/Assignments/AssignmentConte
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI';
import { getAssignmentTask, updateAssignmentTask } from '@services/courses/assignments';
import { getAssignmentTask, getAssignmentTaskSubmissionsMe, handleAssignmentTaskSubmission, updateAssignmentTask } from '@services/courses/assignments';
import { Check, Minus, Plus, PlusCircle, X } from 'lucide-react';
import React, { useEffect, useState } from 'react';
import toast from 'react-hot-toast';
@ -113,6 +113,11 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
questions: [],
submissions: [],
});
const [initialUserSubmissions, setInitialUserSubmissions] = useState<QuizSubmitSchema>({
questions: [],
submissions: [],
});
const [showSavingDisclaimer, setShowSavingDisclaimer] = useState<boolean>(false);
async function chooseOption(qIndex: number, oIndex: number) {
const updatedSubmissions = [...userSubmissions.submissions];
@ -136,11 +141,8 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
...userSubmissions,
submissions: updatedSubmissions,
});
console.log(userSubmissions);
}
async function getAssignmentTaskUI() {
if (assignmentTaskUUID) {
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
@ -151,6 +153,48 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
}
}
async function getAssignmentTaskSubmissionFromUserUI() {
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);
}
}
}
// Detect changes between initial and current submissions
useEffect(() => {
const hasChanges = JSON.stringify(initialUserSubmissions.submissions) !== JSON.stringify(userSubmissions.submissions);
setShowSavingDisclaimer(hasChanges);
}, [userSubmissions, initialUserSubmissions.submissions]);
const submitFC = async () => {
// Save the quiz to the server
const values = {
task_submission: userSubmissions,
grade: 0,
task_submission_grade_feedback: '',
};
if (assignmentTaskUUID) {
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
assignmentTaskStateHook({
type: 'reload',
});
toast.success('Task saved successfully');
setShowSavingDisclaimer(false);
} else {
toast.error('Error saving task, please retry later.');
}
}
};
/* STUDENT VIEW CODE */
useEffect(() => {
@ -164,12 +208,13 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
// Student area
else if (view == 'student') {
getAssignmentTaskUI();
getAssignmentTaskSubmissionFromUserUI();
}
}, [assignmentTaskState, assignment, assignmentTaskStateHook, access_token]);
return (
<AssignmentBoxUI saveFC={saveFC} view={view} type="quiz">
<AssignmentBoxUI submitFC={submitFC} saveFC={saveFC} view={view} showSavingDisclaimer={showSavingDisclaimer} type="quiz">
<div className="flex flex-col space-y-6">
{questions && questions.map((question, qIndex) => (
<div key={qIndex} className="flex flex-col space-y-1.5">
@ -245,8 +290,8 @@ function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
(submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
)
? "bg-green-200/60 text-green-500 hover:bg-green-300" // Selected state colors
: "bg-slate-200/60 text-slate-500 hover:bg-slate-300" // Default state colors
? "bg-green-200/60 text-green-500 hover:bg-green-300" // Selected state colors
: "bg-slate-200/60 text-slate-500 hover:bg-slate-300" // Default state colors
} text-sm transition-all ease-linear cursor-pointer`}>
{userSubmissions.submissions.find(
(submission) =>

View file

@ -93,18 +93,7 @@ function PublishingState() {
{assignment?.assignment_object?.published ? 'Published' : 'Unpublished'}
</div>
<div><EllipsisVertical className='text-gray-500' size={13} /></div>
{assignment?.assignment_object?.published && <ToolTip
side='left'
slateBlack
sideOffset={10}
content="Make your Assignment unavailable for students" >
<div
onClick={() => updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)}
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'>
<BookX size={18} />
<p className='text-sm font-bold'>Unpublish</p>
</div>
</ToolTip>}
<ToolTip
side='left'
slateBlack
@ -118,6 +107,18 @@ function PublishingState() {
<p className=' text-sm font-bold'>Preview</p>
</Link>
</ToolTip>
{assignment?.assignment_object?.published && <ToolTip
side='left'
slateBlack
sideOffset={10}
content="Make your Assignment unavailable for students" >
<div
onClick={() => updateAssignmentPublishState(assignment?.assignment_object?.assignment_uuid)}
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-gray-800 font-medium from-gray-400/50 to-gray-200/80 border border-gray-600/10 shadow-gray-900/10 shadow-lg'>
<BookX size={18} />
<p className='text-sm font-bold'>Unpublish</p>
</div>
</ToolTip>}
{!assignment?.assignment_object?.published &&
<ToolTip
side='left'

View file

@ -1,4 +1,4 @@
import { BookUser, EllipsisVertical, FileUp, Forward, ListTodo, Save } from 'lucide-react'
import { BookUser, EllipsisVertical, FileUp, Forward, Info, InfoIcon, ListTodo, Save } from 'lucide-react'
import React from 'react'
type AssignmentBoxProps = {
@ -6,11 +6,12 @@ type AssignmentBoxProps = {
view?: 'teacher' | 'student'
saveFC?: () => void
submitFC?: () => void
showSavingDisclaimer?: boolean
children: React.ReactNode
}
function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentBoxProps) {
function AssignmentBoxUI({ type, view, saveFC, submitFC, showSavingDisclaimer, children }: AssignmentBoxProps) {
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'>
@ -40,12 +41,17 @@ function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentB
}
</div>
<div className='flex px-1 py-1 rounded-md 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'>
<InfoIcon size={14} />
<p className='text-xs'>Don't forget to save your progress</p>
</div>
}
{/* Save button */}
{view === 'teacher' &&
<div
onClick={() => saveFC && saveFC()}
className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-slate-500 bg-white/60 hover:bg-white/80 linear transition-all nice-shadow '>
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'>
<Save size={14} />
<p className='text-xs font-semibold'>Save</p>
</div>
@ -53,9 +59,9 @@ function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentB
{view === 'student' &&
<div
onClick={() => submitFC && submitFC()}
className='flex px-2 py-1 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-slate-500 bg-white/60 hover:bg-white/80 linear transition-all nice-shadow '>
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'>
<Forward size={14} />
<p className='text-xs font-semibold'>Save</p>
<p className='text-xs font-semibold'>Save your progress</p>
</div>
}

View file

@ -15,7 +15,6 @@ function AssignmentStudentActivity() {
const org = useOrg() as any;
useEffect(() => {
console.log(assignments)
}, [assignments, org])
@ -71,7 +70,14 @@ function AssignmentStudentActivity() {
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'>
<Download size={13} />
<p className='text-xs font-semibold'>Reference file</p>
<div className='flex items-center 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>
</span>
)}
<p className='text-xs font-semibold'>Reference Document</p>
</div>
</Link>
</div>
</div>

View file

@ -91,6 +91,33 @@ export async function getAssignmentTask(
return res
}
export async function getAssignmentTaskSubmissionsMe(
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/submissions/user/me`,
RequestBodyWithAuthHeader('GET', null, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function handleAssignmentTaskSubmission(
body: any,
assignmentTaskUUID: string,
assignmentUUID: string,
access_token: string
) {
const result: any = await fetch(
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/submissions`,
RequestBodyWithAuthHeader('PUT', body, null, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function updateAssignmentTask(
body: any,
assignmentTaskUUID: string,