mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: assignments activity page init
This commit is contained in:
parent
182cd73001
commit
29600d9b6c
12 changed files with 564 additions and 105 deletions
|
|
@ -22,6 +22,7 @@ from src.services.courses.activities.assignments import (
|
|||
delete_assignment_task,
|
||||
delete_assignment_task_submission,
|
||||
put_assignment_task_reference_file,
|
||||
put_assignment_task_submission_file,
|
||||
read_assignment,
|
||||
read_assignment_from_activity_uuid,
|
||||
read_assignment_submissions,
|
||||
|
|
@ -205,6 +206,21 @@ async def api_put_assignment_task_ref_file(
|
|||
request, db_session, assignment_task_uuid, current_user, reference_file
|
||||
)
|
||||
|
||||
@router.post("/{assignment_uuid}/tasks/{assignment_task_uuid}/sub_file")
|
||||
async def api_put_assignment_task_sub_file(
|
||||
request: Request,
|
||||
assignment_task_uuid: str,
|
||||
sub_file: UploadFile | None = None,
|
||||
current_user: PublicUser = Depends(get_current_user),
|
||||
db_session=Depends(get_db_session),
|
||||
):
|
||||
"""
|
||||
Update tasks for an assignment
|
||||
"""
|
||||
return await put_assignment_task_submission_file(
|
||||
request, db_session, assignment_task_uuid, current_user, sub_file
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/{assignment_uuid}/tasks/{assignment_task_uuid}")
|
||||
async def api_delete_assignment_tasks(
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from src.security.rbac.rbac import (
|
|||
authorization_verify_if_element_is_public,
|
||||
authorization_verify_if_user_is_anon,
|
||||
)
|
||||
from src.services.courses.activities.uploads.sub_file import upload_submission_file
|
||||
from src.services.courses.activities.uploads.tasks_ref_files import (
|
||||
upload_reference_file,
|
||||
)
|
||||
|
|
@ -493,6 +494,68 @@ async def put_assignment_task_reference_file(
|
|||
# return assignment task read
|
||||
return AssignmentTaskRead.model_validate(assignment_task)
|
||||
|
||||
async def put_assignment_task_submission_file(
|
||||
request: Request,
|
||||
db_session: Session,
|
||||
assignment_task_uuid: str,
|
||||
current_user: PublicUser | AnonymousUser,
|
||||
sub_file: UploadFile | None = None,
|
||||
):
|
||||
# Check if assignment task exists
|
||||
statement = select(AssignmentTask).where(
|
||||
AssignmentTask.assignment_task_uuid == assignment_task_uuid
|
||||
)
|
||||
assignment_task = db_session.exec(statement).first()
|
||||
|
||||
if not assignment_task:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Assignment Task not found",
|
||||
)
|
||||
|
||||
# Check if assignment exists
|
||||
statement = select(Assignment).where(Assignment.id == assignment_task.assignment_id)
|
||||
assignment = db_session.exec(statement).first()
|
||||
|
||||
if not assignment:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Assignment not found",
|
||||
)
|
||||
|
||||
# Check for activity
|
||||
statement = select(Activity).where(Activity.id == assignment.activity_id)
|
||||
activity = db_session.exec(statement).first()
|
||||
|
||||
# Check if course exists
|
||||
statement = select(Course).where(Course.id == assignment.course_id)
|
||||
course = db_session.exec(statement).first()
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Course not found",
|
||||
)
|
||||
|
||||
# Get org uuid
|
||||
org_statement = select(Organization).where(Organization.id == course.org_id)
|
||||
org = db_session.exec(org_statement).first()
|
||||
|
||||
# RBAC check
|
||||
await rbac_check(request, course.course_uuid, current_user, "read", db_session)
|
||||
|
||||
# Upload reference file
|
||||
if sub_file and sub_file.filename and activity and org:
|
||||
name_in_disk = (
|
||||
f"{assignment_task_uuid}_sub_{current_user.email}_{uuid4()}.{sub_file.filename.split('.')[-1]}"
|
||||
)
|
||||
await upload_submission_file(
|
||||
sub_file, name_in_disk, activity.activity_uuid, org.org_uuid, course.course_uuid, assignment.assignment_uuid, assignment_task_uuid
|
||||
)
|
||||
|
||||
return {"message": "Assignment Task Submission File uploaded"}
|
||||
|
||||
|
||||
|
||||
async def update_assignment_task(
|
||||
request: Request,
|
||||
|
|
|
|||
24
apps/api/src/services/courses/activities/uploads/sub_file.py
Normal file
24
apps/api/src/services/courses/activities/uploads/sub_file.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from uuid import uuid4
|
||||
from src.services.utils.upload_content import upload_content
|
||||
|
||||
|
||||
async def upload_submission_file(
|
||||
file,
|
||||
name_in_disk,
|
||||
activity_uuid,
|
||||
org_uuid,
|
||||
course_uuid,
|
||||
assignment_uuid,
|
||||
assignment_task_uuid,
|
||||
):
|
||||
contents = file.file.read()
|
||||
file_format = file.filename.split(".")[-1]
|
||||
|
||||
await upload_content(
|
||||
f"courses/{course_uuid}/activities/{activity_uuid}/assignments/{assignment_uuid}/tasks/{assignment_task_uuid}/subs",
|
||||
"orgs",
|
||||
org_uuid,
|
||||
contents,
|
||||
f"{name_in_disk}",
|
||||
["pdf", "docx", "mp4", "jpg", "jpeg", "png", "pptx"],
|
||||
)
|
||||
|
|
@ -16,6 +16,11 @@ import { CourseProvider } from '@components/Contexts/CourseContext'
|
|||
import AIActivityAsk from '@components/Objects/Activities/AI/AIActivityAsk'
|
||||
import AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext'
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext'
|
||||
import React, { useEffect } from 'react'
|
||||
import { getAssignmentFromActivityUUID } from '@services/courses/assignments'
|
||||
import AssignmentStudentActivity from '@components/Objects/Activities/Assignment/AssignmentStudentActivity'
|
||||
import { AssignmentProvider } from '@components/Contexts/Assignments/AssignmentContext'
|
||||
import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext'
|
||||
|
||||
interface ActivityClientProps {
|
||||
activityid: string
|
||||
|
|
@ -32,6 +37,11 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
const activity = props.activity
|
||||
const course = props.course
|
||||
const org = useOrg() as any
|
||||
const session = useLHSession() as any;
|
||||
const access_token = session?.data?.tokens?.access_token;
|
||||
const [bgColor, setBgColor] = React.useState('bg-white')
|
||||
const [assignment, setAssignment] = React.useState(null) as any;
|
||||
const [markStatusButtonActive, setMarkStatusButtonActive] = React.useState(false);
|
||||
|
||||
function getChapterNameByActivityId(course: any, activity_id: any) {
|
||||
for (let i = 0; i < course.chapters.length; i++) {
|
||||
|
|
@ -46,6 +56,26 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
return null // return null if no matching activity is found
|
||||
}
|
||||
|
||||
async function getAssignmentUI() {
|
||||
const assignment = await getAssignmentFromActivityUUID(activity.activity_uuid, access_token)
|
||||
setAssignment(assignment.data)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (activity.activity_type == 'TYPE_DYNAMIC') {
|
||||
setBgColor('bg-white nice-shadow');
|
||||
}
|
||||
else if (activity.activity_type == 'TYPE_ASSIGNMENT') {
|
||||
setMarkStatusButtonActive(false);
|
||||
setBgColor('bg-white nice-shadow');
|
||||
getAssignmentUI();
|
||||
}
|
||||
else {
|
||||
setBgColor('bg-zinc-950');
|
||||
}
|
||||
}
|
||||
, [activity])
|
||||
|
||||
return (
|
||||
<>
|
||||
<CourseProvider courseuuid={course?.course_uuid}>
|
||||
|
|
@ -93,6 +123,8 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
</div>
|
||||
<div className="flex space-x-1 items-center">
|
||||
<AuthenticatedClientElement checkMethod="authentication">
|
||||
{activity.activity_type != 'TYPE_ASSIGNMENT' &&
|
||||
<>
|
||||
<AIActivityAsk activity={activity} />
|
||||
<MoreVertical size={17} className="text-gray-300 " />
|
||||
<MarkStatus
|
||||
|
|
@ -101,16 +133,16 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
course={course}
|
||||
orgslug={orgslug}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
|
||||
</AuthenticatedClientElement>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activity ? (
|
||||
<div
|
||||
className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC'
|
||||
? 'bg-white'
|
||||
: 'bg-zinc-950'
|
||||
}`}
|
||||
className={`p-7 drop-shadow-sm rounded-lg ${bgColor}`}
|
||||
>
|
||||
<div>
|
||||
{activity.activity_type == 'TYPE_DYNAMIC' && (
|
||||
|
|
@ -126,6 +158,19 @@ function ActivityClient(props: ActivityClientProps) {
|
|||
activity={activity}
|
||||
/>
|
||||
)}
|
||||
{activity.activity_type == 'TYPE_ASSIGNMENT' && (
|
||||
<div>
|
||||
{assignment ? (
|
||||
<AssignmentProvider assignment_uuid={assignment?.assignment_uuid}>
|
||||
<AssignmentsTaskProvider>
|
||||
<AssignmentStudentActivity />
|
||||
</AssignmentsTaskProvider>
|
||||
</AssignmentProvider>
|
||||
) : (
|
||||
<div></div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ function AssignmentTaskContentEdit() {
|
|||
|
||||
return (
|
||||
<div>
|
||||
{assignment_task?.assignmentTask.assignment_type === 'QUIZ' && <TaskQuizObject />}
|
||||
{assignment_task?.assignmentTask.assignment_type === 'QUIZ' && <TaskQuizObject view='teacher' />}
|
||||
{assignment_task?.assignmentTask.assignment_type === 'FILE_SUBMISSION' && <TaskFileObject view='teacher' />}
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,14 +1,146 @@
|
|||
import AssignmentBoxUI from '@components/Objects/Assignments/AssignmentBoxUI'
|
||||
import { Info } from 'lucide-react'
|
||||
import React from 'react'
|
||||
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
||||
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 { Cloud, File, Info, Loader, UploadCloud } from 'lucide-react'
|
||||
import React, { useEffect } from 'react'
|
||||
import toast from 'react-hot-toast';
|
||||
|
||||
type FileSchema = {
|
||||
fileID: string;
|
||||
};
|
||||
|
||||
type TaskFileObjectProps = {
|
||||
view: 'teacher' | 'student';
|
||||
assignmentTaskUUID?: string;
|
||||
};
|
||||
|
||||
export default function TaskFileObject({ view, assignmentTaskUUID }: TaskFileObjectProps) {
|
||||
const session = useLHSession() as any;
|
||||
const access_token = session?.data?.tokens?.access_token;
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [localUploadFile, setLocalUploadFile] = React.useState<File | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const [assignmentTask, setAssignmentTask] = React.useState<any>(null);
|
||||
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
|
||||
const assignment = useAssignments() as any;
|
||||
|
||||
const handleFileChange = async (event: any) => {
|
||||
const file = event.target.files[0]
|
||||
|
||||
setLocalUploadFile(file)
|
||||
setIsLoading(true)
|
||||
const res = await updateSubFile(
|
||||
file,
|
||||
assignmentTask.assignment_task_uuid,
|
||||
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 {
|
||||
setIsLoading(false)
|
||||
setError('')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function getAssignmentTaskUI() {
|
||||
if (assignmentTaskUUID) {
|
||||
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
|
||||
if (res.success) {
|
||||
setAssignmentTask(res.data);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getAssignmentTaskUI()
|
||||
}
|
||||
, [assignmentTaskUUID])
|
||||
|
||||
export default function TaskFileObject({ view }: any) {
|
||||
return (
|
||||
<AssignmentBoxUI 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} />
|
||||
<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 === '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">
|
||||
{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>
|
||||
)}
|
||||
|
||||
</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>
|
||||
|
||||
<div className='flex space-x-2 mt-2'>
|
||||
|
||||
<File size={20} className='' />
|
||||
<div className='font-semibold text-sm uppercase'>
|
||||
{localUploadFile.name}
|
||||
</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>
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center">
|
||||
<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" />
|
||||
<span>Loading</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex justify-center items-center">
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
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()}
|
||||
>
|
||||
<UploadCloud size={16} className="mr-2" />
|
||||
<span>Submit File</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AssignmentBoxUI>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
||||
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
||||
import { useLHSession } from '@components/Contexts/LHSessionContext';
|
||||
import AssignmentBoxUI from '@components/Objects/Assignments/AssignmentBoxUI';
|
||||
import { updateAssignmentTask } from '@services/courses/assignments';
|
||||
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI';
|
||||
import { getAssignmentTask, 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';
|
||||
|
|
@ -20,14 +20,19 @@ type QuizSchema = {
|
|||
}[];
|
||||
};
|
||||
|
||||
function TaskQuizObject() {
|
||||
type TaskQuizObjectProps = {
|
||||
view: 'teacher' | 'student';
|
||||
assignmentTaskUUID?: string;
|
||||
};
|
||||
|
||||
function TaskQuizObject({ view, assignmentTaskUUID }: TaskQuizObjectProps) {
|
||||
const session = useLHSession() as any;
|
||||
const access_token = session?.data?.tokens?.access_token;
|
||||
const assignmentTaskState = useAssignmentsTask() as any;
|
||||
const assignmentTaskStateHook = useAssignmentsTaskDispatch() as any;
|
||||
const assignment = useAssignments() as any;
|
||||
|
||||
// Teacher area
|
||||
/* TEACHER VIEW CODE */
|
||||
const [questions, setQuestions] = useState<QuizSchema[]>([
|
||||
{ questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] },
|
||||
]);
|
||||
|
|
@ -92,44 +97,73 @@ function TaskQuizObject() {
|
|||
toast.error('Error saving task, please retry later.');
|
||||
}
|
||||
};
|
||||
/* TEACHER VIEW CODE */
|
||||
|
||||
/* STUDENT VIEW CODE */
|
||||
async function getAssignmentTaskUI() {
|
||||
if (assignmentTaskUUID) {
|
||||
const res = await getAssignmentTask(assignmentTaskUUID, access_token);
|
||||
if (res.success) {
|
||||
setQuestions(res.data.contents.questions);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/* STUDENT VIEW CODE */
|
||||
|
||||
useEffect(() => {
|
||||
if (assignmentTaskState.assignmentTask.contents?.questions) {
|
||||
assignmentTaskStateHook({
|
||||
setSelectedAssignmentTaskUUID: assignmentTaskUUID,
|
||||
});
|
||||
// Teacher area
|
||||
if (view == 'teacher' && assignmentTaskState.assignmentTask.contents?.questions) {
|
||||
setQuestions(assignmentTaskState.assignmentTask.contents.questions);
|
||||
}
|
||||
}, [assignmentTaskState,assignment,assignmentTaskStateHook,access_token]);
|
||||
// Student area
|
||||
else if (view == 'student') {
|
||||
getAssignmentTaskUI();
|
||||
}
|
||||
}, [assignmentTaskState, assignment, assignmentTaskStateHook, access_token]);
|
||||
|
||||
// Teacher area end
|
||||
|
||||
return (
|
||||
<AssignmentBoxUI saveFC={saveFC} view='teacher' type="quiz">
|
||||
<AssignmentBoxUI saveFC={saveFC} view={view} type="quiz">
|
||||
<div className="flex flex-col space-y-6">
|
||||
{questions.map((question, qIndex) => (
|
||||
{questions && questions.map((question, qIndex) => (
|
||||
<div key={qIndex} className="flex flex-col space-y-1.5">
|
||||
<div className="flex space-x-2 items-center">
|
||||
{view === 'teacher' ? (
|
||||
<input
|
||||
value={question.questionText}
|
||||
onChange={(e) => handleQuestionChange(qIndex, e.target.value)}
|
||||
placeholder="Question"
|
||||
className="w-full px-3 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"
|
||||
/>
|
||||
) : (
|
||||
<p className="w-full px-3 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold">
|
||||
{question.questionText}
|
||||
</p>
|
||||
)}
|
||||
{view === 'teacher' && (
|
||||
<div
|
||||
className="w-[20px] flex-none flex items-center h-[20px] rounded-lg bg-slate-200/60 text-slate-500 hover:bg-slate-300 text-sm transition-all ease-linear cursor-pointer"
|
||||
onClick={() => removeQuestion(qIndex)}
|
||||
>
|
||||
<Minus size={12} className="mx-auto" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col space-y-2">
|
||||
{question.options.map((option, oIndex) => (
|
||||
<div className="flex" key={oIndex}>
|
||||
<div
|
||||
key={oIndex}
|
||||
className="answer outline outline-3 outline-white pr-2 shadow w-full flex items-center space-x-2 h-[30px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white text-sm duration-150 cursor-pointer ease-linear nice-shadow"
|
||||
>
|
||||
<div className="font-bold text-base flex items-center h-full w-[40px] rounded-l-md text-slate-800 bg-slate-100/80">
|
||||
<p className="mx-auto font-bold text-sm">{String.fromCharCode(65 + oIndex)}</p>
|
||||
</div>
|
||||
{view === 'teacher' ? (
|
||||
<input
|
||||
type="text"
|
||||
value={option.text}
|
||||
|
|
@ -137,6 +171,13 @@ function TaskQuizObject() {
|
|||
placeholder="Option"
|
||||
className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] border-2 border-gray-200 rounded-md border-dotted text-sm font-bold"
|
||||
/>
|
||||
) : (
|
||||
<p className="w-full mx-2 px-3 pr-6 text-neutral-600 bg-[#00008b00] text-sm font-bold">
|
||||
{option.text}
|
||||
</p>
|
||||
)}
|
||||
{view === 'teacher' && (
|
||||
<>
|
||||
<div
|
||||
className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${option.correct ? 'bg-lime-200 text-lime-600' : 'bg-rose-200/60 text-rose-500'
|
||||
} hover:bg-lime-300 text-sm transition-all ease-linear cursor-pointer`}
|
||||
|
|
@ -155,10 +196,10 @@ function TaskQuizObject() {
|
|||
>
|
||||
<Minus size={12} className="mx-auto" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
{/* Show this at the last option */}
|
||||
{oIndex === question.options.length - 1 && (
|
||||
{view === 'teacher' && oIndex === question.options.length - 1 && (
|
||||
<div className="flex justify-center mx-auto px-2">
|
||||
<div
|
||||
className="outline text-xs outline-3 outline-white px-2 shadow w-full flex items-center h-[30px] hover:bg-opacity-100 hover:shadow-md rounded-lg bg-white duration-150 cursor-pointer ease-linear nice-shadow"
|
||||
|
|
@ -170,12 +211,12 @@ function TaskQuizObject() {
|
|||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{view === 'teacher' && (
|
||||
<div className="flex justify-center mx-auto px-2">
|
||||
<div
|
||||
className="flex w-full my-2 py-2 px-4 bg-white text-slate text-xs rounded-md nice-shadow hover:shadow-sm cursor-pointer space-x-3 items-center transition duration-150 ease-linear"
|
||||
|
|
@ -185,6 +226,7 @@ function TaskQuizObject() {
|
|||
<span>Add Question</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AssignmentBoxUI>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
'use client';
|
||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
||||
import { BookOpen, BookX, EllipsisVertical, LayoutList } from 'lucide-react'
|
||||
import { BookOpen, BookX, EllipsisVertical, Eye, LayoutList } from 'lucide-react'
|
||||
import React, { useEffect } from 'react'
|
||||
import { AssignmentProvider, useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
||||
import AssignmentTasks from './_components/Tasks';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { AssignmentsTaskProvider } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip';
|
||||
import AssignmentTaskEditor from './_components/TaskEditor/TaskEditor';
|
||||
|
|
@ -13,6 +12,8 @@ import { useLHSession } from '@components/Contexts/LHSessionContext';
|
|||
import { mutate } from 'swr';
|
||||
import { getAPIUrl } from '@services/config/config';
|
||||
import toast from 'react-hot-toast';
|
||||
import Link from 'next/link';
|
||||
import { useParams } from 'next/navigation';
|
||||
|
||||
function AssignmentEdit() {
|
||||
const params = useParams<{ assignmentuuid: string; }>()
|
||||
|
|
@ -83,7 +84,7 @@ function PublishingState() {
|
|||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log('assignment', assignment?.assignment_object?.assignment_uuid)
|
||||
console.log('assignment', assignment)
|
||||
}, [assignment])
|
||||
|
||||
return (
|
||||
|
|
@ -104,6 +105,19 @@ function PublishingState() {
|
|||
<p className='text-sm font-bold'>Unpublish</p>
|
||||
</div>
|
||||
</ToolTip>}
|
||||
<ToolTip
|
||||
side='left'
|
||||
slateBlack
|
||||
sideOffset={10}
|
||||
content="Preview the Assignment as a student" >
|
||||
<Link
|
||||
target='_blank'
|
||||
href={`/course/${assignment?.course_object?.course_uuid.replace('course_', '')}/activity/${assignment?.activity_object?.activity_uuid.replace('activity_', '')}`}
|
||||
className='flex px-3 py-2 cursor-pointer rounded-md space-x-2 items-center bg-gradient-to-bl text-cyan-800 font-medium from-sky-400/50 to-cyan-200/80 border border-cyan-600/10 shadow-cyan-900/10 shadow-lg'>
|
||||
<Eye size={18} />
|
||||
<p className=' text-sm font-bold'>Preview</p>
|
||||
</Link>
|
||||
</ToolTip>
|
||||
{!assignment?.assignment_object?.published &&
|
||||
<ToolTip
|
||||
side='left'
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export function AssignmentProvider({ children, assignment_uuid }: { children: Re
|
|||
(url) => swrFetcher(url, accessToken)
|
||||
)
|
||||
|
||||
// Define a key for the course object based on assignment data
|
||||
const course_id = assignment?.course_id
|
||||
|
||||
const { data: course_object, error: courseObjectError } = useSWR(
|
||||
|
|
@ -38,12 +37,14 @@ export function AssignmentProvider({ children, assignment_uuid }: { children: Re
|
|||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (assignment && assignment_tasks && (!course_id || course_object) && (!activity_id || activity_object)) {
|
||||
setAssignmentsFull({ assignment_object: assignment, assignment_tasks: assignment_tasks, course_object: course_object, activity_object: activity_object })
|
||||
}, [assignment, assignment_tasks, course_object, activity_object])
|
||||
}
|
||||
}, [assignment, assignment_tasks, course_object, activity_object, course_id, activity_id])
|
||||
|
||||
if (assignmentError || assignmentTasksError || courseObjectError) return <div></div>
|
||||
if (assignmentError || assignmentTasksError || courseObjectError || activityObjectError) return <div></div>
|
||||
|
||||
if (!assignment || !assignment_tasks || (course_id && !course_object)) return <div></div>
|
||||
if (!assignment || !assignment_tasks || (course_id && !course_object) || (activity_id && !activity_object)) return <div></div>
|
||||
|
||||
return <AssignmentContext.Provider value={assignmentsFull}>{children}</AssignmentContext.Provider>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
import { BookUser, EllipsisVertical, FileUp, ListTodo, Save } from 'lucide-react'
|
||||
import { BookUser, EllipsisVertical, FileUp, Forward, ListTodo, Save } from 'lucide-react'
|
||||
import React from 'react'
|
||||
|
||||
type AssignmentBoxProps = {
|
||||
type: 'quiz' | 'file'
|
||||
view?: 'teacher' | 'student'
|
||||
saveFC?: () => void
|
||||
submitFC?: () => void
|
||||
children: React.ReactNode
|
||||
|
||||
}
|
||||
|
||||
function AssignmentBoxUI({ type, view, saveFC, children }: AssignmentBoxProps) {
|
||||
function AssignmentBoxUI({ type, view, saveFC, submitFC, children }: AssignmentBoxProps) {
|
||||
return (
|
||||
<div className='flex flex-col px-4 py-2 nice-shadow rounded-md bg-slate-100/30'>
|
||||
<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='text-lg font-semibold'>
|
||||
|
|
@ -41,12 +42,22 @@ function AssignmentBoxUI({ type, view, saveFC, children }: AssignmentBoxProps) {
|
|||
<div className='flex px-1 py-1 rounded-md items-center'>
|
||||
|
||||
{/* 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 '>
|
||||
<Save size={14} />
|
||||
<p className='text-xs font-semibold'>Save</p>
|
||||
</div>
|
||||
}
|
||||
{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 '>
|
||||
<Forward size={14} />
|
||||
<p className='text-xs font-semibold'>Save</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
|
||||
import { useAssignmentsTask } from '@components/Contexts/Assignments/AssignmentsTaskContext';
|
||||
import { useCourse } from '@components/Contexts/CourseContext';
|
||||
import { useOrg } from '@components/Contexts/OrgContext';
|
||||
import { getTaskRefFileDir } from '@services/media/media';
|
||||
import TaskFileObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskFileObject';
|
||||
import TaskQuizObject from 'app/orgs/[orgslug]/dash/assignments/[assignmentuuid]/_components/TaskEditor/Subs/TaskTypes/TaskQuizObject'
|
||||
import { Backpack, Calendar, Download, EllipsisVertical, Info } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
function AssignmentStudentActivity() {
|
||||
const assignments = useAssignments() as any;
|
||||
const course = useCourse() as any;
|
||||
const org = useOrg() as any;
|
||||
|
||||
useEffect(() => {
|
||||
console.log(assignments)
|
||||
}, [assignments, org])
|
||||
|
||||
|
||||
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} />
|
||||
<p className='font-semibold'>Assignment</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className='flex space-x-2 items-center'>
|
||||
<EllipsisVertical className='text-slate-400' size={18} />
|
||||
<div className='flex space-x-2 items-center'>
|
||||
<div className='flex 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>
|
||||
</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'>
|
||||
<p>Task {index + 1} : </p>
|
||||
<p className='text-slate-500'>{task.description}</p>
|
||||
</div>
|
||||
<div className='flex space-x-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'>
|
||||
<Info size={13} />
|
||||
<p className='text-xs font-semibold'>View Hint</p>
|
||||
</div>
|
||||
<Link
|
||||
href={getTaskRefFileDir(
|
||||
org?.org_uuid,
|
||||
assignments?.course_object.course_uuid,
|
||||
assignments?.activity_object.activity_uuid,
|
||||
assignments?.assignment_object.assignment_uuid,
|
||||
task.assignment_task_uuid,
|
||||
task.reference_file
|
||||
)}
|
||||
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'>
|
||||
<Download size={13} />
|
||||
<p className='text-xs font-semibold'>Reference file</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{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>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AssignmentStudentActivity
|
||||
|
|
@ -138,3 +138,25 @@ export async function updateReferenceFile(
|
|||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
||||
|
||||
export async function updateSubFile(
|
||||
file: any,
|
||||
assignmentTaskUUID: string,
|
||||
assignmentUUID: string,
|
||||
access_token: string
|
||||
) {
|
||||
|
||||
// Send file thumbnail as form data
|
||||
const formData = new FormData()
|
||||
|
||||
if (file) {
|
||||
formData.append('sub_file', file)
|
||||
}
|
||||
const result: any = await fetch(
|
||||
`${getAPIUrl()}assignments/${assignmentUUID}/tasks/${assignmentTaskUUID}/sub_file`,
|
||||
RequestBodyFormWithAuthHeader('POST', formData, null, access_token)
|
||||
)
|
||||
const res = await getResponseMetadata(result)
|
||||
return res
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue