mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: enable Tasks submissions
This commit is contained in:
parent
6d7e521bba
commit
bfb977ac5d
9 changed files with 330 additions and 110 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue