Merge pull request #307 from learnhouse/feat/fix-bugs

Fix Various Bugs
This commit is contained in:
Badr B. 2024-08-29 20:23:44 +02:00 committed by GitHub
commit 1bc078bd0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 3053 additions and 818 deletions

View file

@ -0,0 +1,47 @@
"""Org IDs Cascades
Revision ID: 83b6d9d6f57a
Revises: cb2029aadc2d
Create Date: 2024-08-29 19:38:10.022100
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa # noqa: F401
import sqlmodel # noqa: F401
# revision identifiers, used by Alembic.
revision: str = '83b6d9d6f57a'
down_revision: Union[str, None] = 'cb2029aadc2d'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('chapter_org_id_fkey', 'chapter', type_='foreignkey')
op.drop_constraint('chapteractivity_org_id_fkey', 'chapteractivity', type_='foreignkey')
op.create_foreign_key(None, 'chapteractivity', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.drop_constraint('collectioncourse_org_id_fkey', 'collectioncourse', type_='foreignkey')
op.create_foreign_key(None, 'collectioncourse', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.drop_constraint('coursechapter_org_id_fkey', 'coursechapter', type_='foreignkey')
op.create_foreign_key(None, 'coursechapter', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.drop_constraint('courseupdate_org_id_fkey', 'courseupdate', type_='foreignkey')
op.create_foreign_key(None, 'courseupdate', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'courseupdate', type_='foreignkey')
op.create_foreign_key('courseupdate_org_id_fkey', 'courseupdate', 'organization', ['org_id'], ['id'])
op.drop_constraint(None, 'coursechapter', type_='foreignkey')
op.create_foreign_key('coursechapter_org_id_fkey', 'coursechapter', 'organization', ['org_id'], ['id'])
op.drop_constraint(None, 'collectioncourse', type_='foreignkey')
op.create_foreign_key('collectioncourse_org_id_fkey', 'collectioncourse', 'organization', ['org_id'], ['id'])
op.drop_constraint(None, 'chapteractivity', type_='foreignkey')
op.create_foreign_key('chapteractivity_org_id_fkey', 'chapteractivity', 'organization', ['org_id'], ['id'])
op.create_foreign_key('chapter_org_id_fkey', 'chapter', 'organization', ['org_id'], ['id'])
# ### end Alembic commands ###

View file

@ -0,0 +1,153 @@
"""Cloud Changes
Revision ID: cb2029aadc2d
Revises: d8bc71595932
Create Date: 2024-08-29 19:24:34.859544
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa # noqa: F401
import sqlmodel # noqa: F401
# revision identifiers, used by Alembic.
revision: str = 'cb2029aadc2d'
down_revision: Union[str, None] = 'd8bc71595932'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('activity', 'course_id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
op.drop_constraint('activity_org_id_fkey', 'activity', type_='foreignkey')
op.create_foreign_key(None, 'activity', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.drop_constraint('block_org_id_fkey', 'block', type_='foreignkey')
op.create_foreign_key(None, 'block', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.alter_column('collection', 'org_id',
existing_type=sa.INTEGER(),
type_=sa.BigInteger(),
existing_nullable=True)
op.drop_constraint('collection_org_id_fkey', 'collection', type_='foreignkey')
op.create_foreign_key(None, 'collection', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.alter_column('collectioncourse', 'collection_id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
op.alter_column('collectioncourse', 'course_id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
op.drop_constraint('course_org_id_fkey', 'course', type_='foreignkey')
op.create_foreign_key(None, 'course', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.alter_column('coursechapter', 'course_id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
op.alter_column('coursechapter', 'chapter_id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
op.drop_constraint('resourceauthor_user_id_fkey', 'resourceauthor', type_='foreignkey')
op.create_foreign_key(None, 'resourceauthor', 'user', ['user_id'], ['id'], ondelete='CASCADE')
op.drop_constraint('role_org_id_fkey', 'role', type_='foreignkey')
op.create_foreign_key(None, 'role', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.drop_constraint('trailrun_user_id_fkey', 'trailrun', type_='foreignkey')
op.drop_constraint('trailrun_course_id_fkey', 'trailrun', type_='foreignkey')
op.drop_constraint('trailrun_trail_id_fkey', 'trailrun', type_='foreignkey')
op.drop_constraint('trailrun_org_id_fkey', 'trailrun', type_='foreignkey')
op.create_foreign_key(None, 'trailrun', 'user', ['user_id'], ['id'], ondelete='CASCADE')
op.create_foreign_key(None, 'trailrun', 'course', ['course_id'], ['id'], ondelete='CASCADE')
op.create_foreign_key(None, 'trailrun', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.create_foreign_key(None, 'trailrun', 'trail', ['trail_id'], ['id'], ondelete='CASCADE')
op.alter_column('trailstep', 'trailrun_id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
op.drop_constraint('trailstep_activity_id_fkey', 'trailstep', type_='foreignkey')
op.drop_constraint('trailstep_org_id_fkey', 'trailstep', type_='foreignkey')
op.drop_constraint('trailstep_course_id_fkey', 'trailstep', type_='foreignkey')
op.drop_constraint('trailstep_user_id_fkey', 'trailstep', type_='foreignkey')
op.drop_constraint('trailstep_trail_id_fkey', 'trailstep', type_='foreignkey')
op.create_foreign_key(None, 'trailstep', 'organization', ['org_id'], ['id'], ondelete='CASCADE')
op.create_foreign_key(None, 'trailstep', 'user', ['user_id'], ['id'], ondelete='CASCADE')
op.create_foreign_key(None, 'trailstep', 'trail', ['trail_id'], ['id'], ondelete='CASCADE')
op.create_foreign_key(None, 'trailstep', 'course', ['course_id'], ['id'], ondelete='CASCADE')
op.create_foreign_key(None, 'trailstep', 'activity', ['activity_id'], ['id'], ondelete='CASCADE')
op.alter_column('userorganization', 'org_id',
existing_type=sa.BIGINT(),
type_=sa.Integer(),
existing_nullable=True)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('userorganization', 'org_id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
op.drop_constraint(None, 'trailstep', type_='foreignkey')
op.drop_constraint(None, 'trailstep', type_='foreignkey')
op.drop_constraint(None, 'trailstep', type_='foreignkey')
op.drop_constraint(None, 'trailstep', type_='foreignkey')
op.drop_constraint(None, 'trailstep', type_='foreignkey')
op.create_foreign_key('trailstep_trail_id_fkey', 'trailstep', 'trail', ['trail_id'], ['id'])
op.create_foreign_key('trailstep_user_id_fkey', 'trailstep', 'user', ['user_id'], ['id'])
op.create_foreign_key('trailstep_course_id_fkey', 'trailstep', 'course', ['course_id'], ['id'])
op.create_foreign_key('trailstep_org_id_fkey', 'trailstep', 'organization', ['org_id'], ['id'])
op.create_foreign_key('trailstep_activity_id_fkey', 'trailstep', 'activity', ['activity_id'], ['id'])
op.alter_column('trailstep', 'trailrun_id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
op.drop_constraint(None, 'trailrun', type_='foreignkey')
op.drop_constraint(None, 'trailrun', type_='foreignkey')
op.drop_constraint(None, 'trailrun', type_='foreignkey')
op.drop_constraint(None, 'trailrun', type_='foreignkey')
op.create_foreign_key('trailrun_org_id_fkey', 'trailrun', 'organization', ['org_id'], ['id'])
op.create_foreign_key('trailrun_trail_id_fkey', 'trailrun', 'trail', ['trail_id'], ['id'])
op.create_foreign_key('trailrun_course_id_fkey', 'trailrun', 'course', ['course_id'], ['id'])
op.create_foreign_key('trailrun_user_id_fkey', 'trailrun', 'user', ['user_id'], ['id'])
op.drop_constraint(None, 'role', type_='foreignkey')
op.create_foreign_key('role_org_id_fkey', 'role', 'organization', ['org_id'], ['id'])
op.drop_constraint(None, 'resourceauthor', type_='foreignkey')
op.create_foreign_key('resourceauthor_user_id_fkey', 'resourceauthor', 'user', ['user_id'], ['id'])
op.alter_column('coursechapter', 'chapter_id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
op.alter_column('coursechapter', 'course_id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
op.drop_constraint(None, 'course', type_='foreignkey')
op.create_foreign_key('course_org_id_fkey', 'course', 'organization', ['org_id'], ['id'])
op.alter_column('collectioncourse', 'course_id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
op.alter_column('collectioncourse', 'collection_id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
op.drop_constraint(None, 'collection', type_='foreignkey')
op.create_foreign_key('collection_org_id_fkey', 'collection', 'organization', ['org_id'], ['id'])
op.alter_column('collection', 'org_id',
existing_type=sa.BigInteger(),
type_=sa.INTEGER(),
existing_nullable=True)
op.drop_constraint(None, 'block', type_='foreignkey')
op.create_foreign_key('block_org_id_fkey', 'block', 'organization', ['org_id'], ['id'])
op.drop_constraint(None, 'activity', type_='foreignkey')
op.create_foreign_key('activity_org_id_fkey', 'activity', 'organization', ['org_id'], ['id'])
op.alter_column('activity', 'course_id',
existing_type=sa.Integer(),
type_=sa.BIGINT(),
existing_nullable=True)
# ### end Alembic commands ###

View file

@ -11,6 +11,8 @@ class CollectionCourse(SQLModel, table=True):
course_id: int = Field(
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
)
org_id: int = Field(default=None, foreign_key="organization.id")
org_id: int = Field(
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
creation_date: str
update_date: str

View file

@ -1,5 +1,5 @@
from typing import Optional
from sqlalchemy import BigInteger, Column, ForeignKey
from sqlalchemy import BigInteger, Column, ForeignKey, Integer
from sqlmodel import Field, SQLModel
class ChapterActivity(SQLModel, table=True):
@ -8,6 +8,8 @@ class ChapterActivity(SQLModel, table=True):
chapter_id: int = Field(sa_column=Column(BigInteger, ForeignKey("chapter.id", ondelete="CASCADE")))
activity_id: int = Field(sa_column=Column(BigInteger, ForeignKey("activity.id", ondelete="CASCADE")))
course_id : int = Field(sa_column=Column(BigInteger, ForeignKey("course.id", ondelete="CASCADE")))
org_id : int = Field(default=None, foreign_key="organization.id")
org_id: int = Field(
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
creation_date: str
update_date: str

View file

@ -9,7 +9,9 @@ class ChapterBase(SQLModel):
name: str
description: Optional[str] = ""
thumbnail_image: Optional[str] = ""
org_id: int = Field(default=None, foreign_key="organization.id")
org_id: int = Field(
sa_column=Column("org_id", ForeignKey("organization.id", ondelete="CASCADE"))
)
course_id: int = Field(
sa_column=Column("course_id", ForeignKey("course.id", ondelete="CASCADE"))
)

View file

@ -12,6 +12,8 @@ class CourseChapter(SQLModel, table=True):
chapter_id: int = Field(
sa_column=Column(Integer, ForeignKey("chapter.id", ondelete="CASCADE"))
)
org_id: int = Field(default=None, foreign_key="organization.id")
org_id: int = Field(
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
creation_date: str
update_date: str

View file

@ -12,7 +12,9 @@ class CourseUpdate(SQLModel, table=True):
sa_column=Column(Integer, ForeignKey("course.id", ondelete="CASCADE"))
)
linked_activity_uuids: Optional[str] = Field(default=None)
org_id: int = Field(default=None, foreign_key="organization.id")
org_id: int = Field(
sa_column=Column(Integer, ForeignKey("organization.id", ondelete="CASCADE"))
)
creation_date: str
update_date: str

View file

@ -34,6 +34,7 @@ from src.services.orgs.orgs import (
get_organization,
get_organization_by_slug,
get_orgs_by_user,
get_orgs_by_user_admin,
update_org,
update_org_logo,
update_org_signup_mechanism,
@ -329,6 +330,22 @@ async def api_user_orgs(
)
@router.get("/user_admin/page/{page}/limit/{limit}")
async def api_user_orgs_admin(
request: Request,
page: int,
limit: int,
current_user: PublicUser = Depends(get_current_user),
db_session: Session = Depends(get_db_session),
) -> List[OrganizationRead]:
"""
Get orgs by page and limit by current user
"""
return await get_orgs_by_user_admin(
request, db_session, str(current_user.id), page, limit
)
@router.put("/{org_id}")
async def api_update_org(
request: Request,

View file

@ -330,7 +330,7 @@ def install_create_organization(org_object: OrganizationCreate, db_session: Sess
# Org Config
org_config = OrganizationConfigBase(
config_version="1.0",
config_version="1.1",
general=OrgGeneralConfig(
enabled=True,
color="normal",

View file

@ -377,7 +377,7 @@ def send_invite_email(
<body>
<p>Hello {email}</p>
<p>You have been invited to {org.name} by @{user.username}. Your invite code is {invite['invite_code']}.</p>
<p>Click <a href="{org.slug}.learnhouse.io/signup?inviteCode={invite['invite_code']}">here</a> to sign up.</p>
<p>Click <a href="{org.slug}.learnhouse.io/signup?orgslug={org.slug}&inviteCode={invite['invite_code']}">here</a> to sign up.</p>
<p>Thank you</p>
</body>
</html>

View file

@ -158,7 +158,7 @@ async def create_org(
db_session.refresh(user_org)
org_config = org_config = OrganizationConfigBase(
config_version="1.0",
config_version="1.",
general=OrgGeneralConfig(
enabled=True,
color="normal",
@ -179,10 +179,7 @@ async def create_org(
collaboration=CollaborationOrgConfig(enabled=True, limit=0),
api=APIOrgConfig(enabled=True, limit=0),
),
cloud=OrgCloudConfig(
plan='free',
custom_domain=False
)
cloud=OrgCloudConfig(plan="free", custom_domain=False),
)
org_config = json.loads(org_config.json())
@ -463,7 +460,7 @@ async def delete_org(
return {"detail": "Organization deleted"}
async def get_orgs_by_user(
async def get_orgs_by_user_admin(
request: Request,
db_session: Session,
user_id: str,
@ -507,6 +504,47 @@ async def get_orgs_by_user(
return orgsWithConfig
async def get_orgs_by_user(
request: Request,
db_session: Session,
user_id: str,
page: int = 1,
limit: int = 10,
) -> list[OrganizationRead]:
statement = (
select(Organization)
.join(UserOrganization)
.where(UserOrganization.user_id == user_id)
.offset((page - 1) * limit)
.limit(limit)
)
# Get organizations where the user is an admin
result = db_session.exec(statement)
orgs = result.all()
orgsWithConfig = []
for org in orgs:
# Get org config
statement = select(OrganizationConfig).where(
OrganizationConfig.org_id == org.id
)
result = db_session.exec(statement)
org_config = result.first()
config = OrganizationConfig.model_validate(org_config) if org_config else {}
org = OrganizationRead(**org.model_dump(), config=config)
orgsWithConfig.append(org)
return orgsWithConfig
# Config related
async def update_org_signup_mechanism(
request: Request,

2
apps/web/.gitignore vendored
View file

@ -45,3 +45,5 @@ next.config.original.js
.sentryclirc
certificates
# Sentry Config File
.env.sentry-build-plugin

View file

@ -170,7 +170,7 @@ const LoginClient = (props: LoginClientProps) => {
</FormField>
<div>
<Link
href={getUriWithOrg(props.org.slug, '/forgot')}
href={{ pathname: getUriWithoutOrg('/forgot'), query: props.org.slug ? { orgslug: props.org.slug } : null }}
passHref
className="text-xs text-gray-500 hover:underline"
>

View file

@ -0,0 +1,23 @@
"use client";
import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";
export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return (
<html>
<body>
{/* `NextError` is the default Next.js error page component. Its type
definition requires a `statusCode` prop. However, since the App Router
does not expose status codes for errors, we simply pass 0 to render a
generic error message. */}
<NextError statusCode={0} />
</body>
</html>
);
}

View file

@ -1,5 +1,5 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { useAssignmentsTask, useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useAssignmentsTaskDispatch } from '@components/Contexts/Assignments/AssignmentsTaskContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import { useOrg } from '@components/Contexts/OrgContext';
import AssignmentBoxUI from '@components/Objects/Activities/Assignment/AssignmentBoxUI'

View file

@ -16,7 +16,7 @@ type QuizSchema = {
text: string;
fileID: string;
type: 'text' | 'image' | 'audio' | 'video';
correct: boolean;
assigned_right_answer: boolean;
}[];
};
@ -25,6 +25,7 @@ type QuizSubmitSchema = {
submissions: {
questionUUID: string;
optionUUID: string;
answer: boolean
}[];
};
@ -34,6 +35,12 @@ type TaskQuizObjectProps = {
assignmentTaskUUID?: string;
};
type Submission = {
questionUUID: string;
optionUUID: string;
answer: boolean;
};
function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectProps) {
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
@ -44,7 +51,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
/* TEACHER VIEW CODE */
const [questions, setQuestions] = useState<QuizSchema[]>([
{ questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] },
{ questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', assigned_right_answer: false, optionUUID: 'option_' + uuidv4() }] },
]);
const handleQuestionChange = (index: number, value: string) => {
@ -61,7 +68,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
const addOption = (qIndex: number) => {
const updatedQuestions = [...questions];
updatedQuestions[qIndex].options.push({ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() });
updatedQuestions[qIndex].options.push({ text: '', fileID: '', type: 'text', assigned_right_answer: false, optionUUID: 'option_' + uuidv4() });
setQuestions(updatedQuestions);
};
@ -72,7 +79,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
};
const addQuestion = () => {
setQuestions([...questions, { questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', correct: false, optionUUID: 'option_' + uuidv4() }] }]);
setQuestions([...questions, { questionText: '', questionUUID: 'question_' + uuidv4(), options: [{ text: '', fileID: '', type: 'text', assigned_right_answer: false, optionUUID: 'option_' + uuidv4() }] }]);
};
const removeQuestion = (qIndex: number) => {
@ -81,12 +88,12 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
setQuestions(updatedQuestions);
};
const toggleCorrectOption = (qIndex: number, oIndex: number) => {
const toggleOption = (qIndex: number, oIndex: number) => {
const updatedQuestions = [...questions];
// Find the option to toggle
const optionToToggle = updatedQuestions[qIndex].options[oIndex];
// Toggle the 'correct' property of the option
optionToToggle.correct = !optionToToggle.correct;
optionToToggle.assigned_right_answer = !optionToToggle.assigned_right_answer;
setQuestions(updatedQuestions);
};
@ -123,20 +130,24 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
async function chooseOption(qIndex: number, oIndex: number) {
const updatedSubmissions = [...userSubmissions.submissions];
const questionUUID = questions[qIndex].questionUUID;
const optionUUID = questions[qIndex].options[oIndex].optionUUID;
const question = questions[qIndex];
const option = question?.options[oIndex];
// Check if this question already has a submission with the selected option
const existingSubmissionIndex = updatedSubmissions.findIndex(
if (!question || !option) return;
const questionUUID = question.questionUUID;
const optionUUID = option.optionUUID;
if (!questionUUID || !optionUUID) return;
const submissionIndex = updatedSubmissions.findIndex(
(submission) => submission.questionUUID === questionUUID && submission.optionUUID === optionUUID
);
if (existingSubmissionIndex === -1 && optionUUID && questionUUID) {
// If the selected option is not already chosen, add it to the submissions
updatedSubmissions.push({ questionUUID, optionUUID });
if (submissionIndex === -1) {
updatedSubmissions.push({ questionUUID, optionUUID, answer: true });
} else {
// If the selected option is already chosen, remove it from the submissions
updatedSubmissions.splice(existingSubmissionIndex, 1);
updatedSubmissions[submissionIndex].answer = !updatedSubmissions[submissionIndex].answer;
}
setUserSubmissions({
@ -176,12 +187,34 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
const submitFC = async () => {
// Ensure all questions and options have submissions
const updatedSubmissions: Submission[] = questions.flatMap(question => {
return question.options.map(option => {
const existingSubmission = userSubmissions.submissions.find(
submission => submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
);
return existingSubmission || {
questionUUID: question.questionUUID || '',
optionUUID: option.optionUUID || '',
answer: false // Mark unsubmitted options as false
};
});
});
// Update userSubmissions with the complete set of submissions
const updatedUserSubmissions: QuizSubmitSchema = {
...userSubmissions,
submissions: updatedSubmissions
};
// Save the quiz to the server
const values = {
task_submission: userSubmissions,
task_submission: updatedUserSubmissions,
grade: 0,
task_submission_grade_feedback: '',
};
if (assignmentTaskUUID) {
const res = await handleAssignmentTaskSubmission(values, assignmentTaskUUID, assignment.assignment_object.assignment_uuid, access_token);
if (res) {
@ -190,6 +223,7 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
});
toast.success('Task saved successfully');
setShowSavingDisclaimer(false);
setUserSubmissions(updatedUserSubmissions);
} else {
toast.error('Error saving task, please retry later.');
}
@ -214,30 +248,22 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
async function gradeFC() {
if (assignmentTaskUUID) {
// Ensure maxPoints is defined
const maxPoints = assignmentTaskOutsideProvider?.max_grade_value || 100; // Default to 100 if not defined
const maxPoints = assignmentTaskOutsideProvider?.max_grade_value || 100;
const totalOptions = questions.reduce((total, question) => total + question.options.length, 0);
let correctAnswers = 0;
// Ensure userSubmissions.questions are set
const totalQuestions = questions.length;
let correctQuestions = 0;
let incorrectQuestions = 0;
userSubmissions.submissions.forEach((submission) => {
const question = questions.find((q) => q.questionUUID === submission.questionUUID);
const option = question?.options.find((o) => o.optionUUID === submission.optionUUID);
if (option?.correct) {
correctQuestions++;
} else {
incorrectQuestions++;
questions.forEach((question) => {
question.options.forEach((option) => {
const submission = userSubmissions.submissions.find(
(sub) => sub.questionUUID === question.questionUUID && sub.optionUUID === option.optionUUID
);
if (submission?.answer === option.assigned_right_answer) {
correctAnswers++;
}
});
});
// Calculate grade with penalties for incorrect answers
const pointsPerQuestion = maxPoints / totalQuestions;
const rawGrade = (correctQuestions - incorrectQuestions) * pointsPerQuestion;
// Ensure the grade is within the valid range
const finalGrade = Math.max(0, Math.min(rawGrade, maxPoints));
const finalGrade = Math.round((correctAnswers / totalOptions) * maxPoints);
// Save the grade to the server
const values = {
@ -337,15 +363,15 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
{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'
className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${option.assigned_right_answer ? '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`}
onClick={() => toggleCorrectOption(qIndex, oIndex)}
onClick={() => toggleOption(qIndex, oIndex)}
>
{option.correct ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
{option.correct ? (
<p className="mx-auto font-bold text-xs">Correct</p>
{option.assigned_right_answer ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
{option.assigned_right_answer ? (
<p className="mx-auto font-bold text-xs">True</p>
) : (
<p className="mx-auto font-bold text-xs">Incorrect</p>
<p className="mx-auto font-bold text-xs">False</p>
)}
</div>
<div
@ -359,30 +385,38 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
{view === 'grading' && (
<>
<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'
className={`w-fit flex-none flex text-xs px-2 py-0.5 space-x-1 items-center h-fit rounded-lg ${option.assigned_right_answer ? '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`}
>
{option.correct ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
{option.correct ? (
<p className="mx-auto font-bold text-xs">Marked as Correct</p>
{option.assigned_right_answer ? <Check size={12} className="mx-auto" /> : <X size={12} className="mx-auto" />}
{option.assigned_right_answer ? (
<p className="mx-auto font-bold text-xs">Marked as True</p>
) : (
<p className="mx-auto font-bold text-xs">Marked as Incorrect</p>
<p className="mx-auto font-bold text-xs">Marked as False</p>
)}
</div>
</>
)}
{view === 'student' && (
<div className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${userSubmissions.submissions.find(
<div
className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${
userSubmissions.submissions.find(
(submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
submission.questionUUID === question.questionUUID &&
submission.optionUUID === option.optionUUID &&
submission.answer
)
? "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`}>
? "bg-green-200/60 text-green-500 hover:bg-green-300"
: "bg-slate-200/60 text-slate-500 hover:bg-slate-300"
} text-sm transition-all ease-linear cursor-pointer`}
onClick={() => chooseOption(qIndex, oIndex)}
>
{userSubmissions.submissions.find(
(submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
submission.questionUUID === question.questionUUID &&
submission.optionUUID === option.optionUUID &&
submission.answer
) ? (
<Check size={12} className="mx-auto" />
) : (
@ -391,22 +425,30 @@ function TaskQuizObject({ view, assignmentTaskUUID, user_id }: TaskQuizObjectPro
</div>
)}
{view === 'grading' && (
<div className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${userSubmissions.submissions.find(
<>
<div className={`w-[20px] flex-none flex items-center h-[20px] rounded-lg ${
userSubmissions.submissions.find(
(submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
submission.questionUUID === question.questionUUID &&
submission.optionUUID === option.optionUUID &&
submission.answer
)
? "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`}>
? "bg-green-200/60 text-green-500"
: "bg-slate-200/60 text-slate-500"
} text-sm`}>
{userSubmissions.submissions.find(
(submission) =>
submission.questionUUID === question.questionUUID && submission.optionUUID === option.optionUUID
submission.questionUUID === question.questionUUID &&
submission.optionUUID === option.optionUUID &&
submission.answer
) ? (
<Check size={12} className="mx-auto" />
) : (
<X size={12} className="mx-auto" />
)}
</div>
</>
)}
</div>

View file

@ -4,7 +4,7 @@ import Modal from '@components/StyledElements/Modal/Modal';
import { getAPIUrl } from '@services/config/config';
import { getUserAvatarMediaDirectory } from '@services/media/media';
import { swrFetcher } from '@services/utils/ts/requests';
import { Loader, SendHorizonal, UserCheck, X } from 'lucide-react';
import { SendHorizonal, UserCheck, X } from 'lucide-react';
import React, { useEffect } from 'react';
import useSWR from 'swr';
import EvaluateAssignment from './Modals/EvaluateAssignment';

View file

@ -1,5 +1,5 @@
import { useAssignments } from '@components/Contexts/Assignments/AssignmentContext';
import { Apple, ArrowRightFromLine, BookOpenCheck, Check, Download, Info, Medal, MoveRight, X } from 'lucide-react';
import { BookOpenCheck, Check, Download, Info, MoveRight, X } from 'lucide-react';
import Link from 'next/link';
import React from 'react'
import TaskQuizObject from '../../_components/TaskEditor/Subs/TaskTypes/TaskQuizObject';

View file

@ -6,7 +6,7 @@ import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import { getAssignmentsFromACourse } from '@services/courses/assignments';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import { swrFetcher } from '@services/utils/ts/requests';
import { Book, EllipsisVertical, GalleryVertical, GalleryVerticalEnd, Info, Layers2, PenBox, UserRoundPen } from 'lucide-react';
import { EllipsisVertical, GalleryVerticalEnd, Info, Layers2, UserRoundPen } from 'lucide-react';
import Link from 'next/link';
import React from 'react'
import useSWR from 'swr';

View file

@ -1,5 +1,4 @@
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';

View file

@ -32,23 +32,19 @@ function QuizBlockComponent(props: any) {
const isEditable = editorState.isEditable
const handleAnswerClick = (question_id: string, answer_id: string) => {
// if the quiz is submitted, do nothing
if (submitted) {
return
if (submitted) return;
const existingAnswerIndex = userAnswers.findIndex(
(answer: any) => answer.question_id === question_id && answer.answer_id === answer_id
);
if (existingAnswerIndex !== -1) {
// Remove the answer if it's already selected
setUserAnswers(userAnswers.filter((_, index) => index !== existingAnswerIndex));
} else {
// Add the answer
setUserAnswers([...userAnswers, { question_id, answer_id }]);
}
const userAnswer = {
question_id: question_id,
answer_id: answer_id,
}
const newAnswers = [...userAnswers, userAnswer]
// only accept one answer per question
const filteredAnswers = newAnswers.filter(
(answer: any) => answer.question_id !== question_id
)
setUserAnswers([...filteredAnswers, userAnswer])
}
const refreshUserSubmission = () => {
@ -57,38 +53,31 @@ function QuizBlockComponent(props: any) {
}
const handleUserSubmission = () => {
if (userAnswers.length === 0) {
setSubmissionMessage('Please answer at least one question!')
return
}
setSubmitted(true);
setSubmitted(true)
// check if all submitted answers are correct
const correctAnswers = questions.map((question: Question) => {
const correctAnswer: any = question.answers.find(
(answer: Answer) => answer.correct
)
const userAnswer = userAnswers.find(
const correctAnswers = questions.every((question: Question) => {
const correctAnswers = question.answers.filter((answer: Answer) => answer.correct);
const userAnswersForQuestion = userAnswers.filter(
(userAnswer: any) => userAnswer.question_id === question.question_id
)
if (correctAnswer.answer_id === userAnswer.answer_id) {
return true
} else {
return false
}
})
);
// check if all answers are correct
const allCorrect = correctAnswers.every(
(answer: boolean) => answer === true
)
if (allCorrect) {
setSubmissionMessage('All answers are correct!')
} else {
setSubmissionMessage('Some answers are incorrect!')
// If no correct answers are set and user didn't select any, it's correct
if (correctAnswers.length === 0 && userAnswersForQuestion.length === 0) {
return true;
}
// Check if user selected all correct answers and no incorrect ones
return (
correctAnswers.length === userAnswersForQuestion.length &&
correctAnswers.every((correctAnswer: Answer) =>
userAnswersForQuestion.some(
(userAnswer: any) => userAnswer.answer_id === correctAnswer.answer_id
)
)
);
});
setSubmissionMessage(correctAnswers ? 'All answers are correct!' : 'Some answers are incorrect!');
}
const getAnswerID = (answerIndex: number, questionId: string) => {
@ -204,19 +193,14 @@ function QuizBlockComponent(props: any) {
const markAnswerCorrect = (question_id: string, answer_id: string) => {
const newQuestions = questions.map((question: Question) => {
if (question.question_id === question_id) {
question.answers.map((answer: Answer) => {
if (answer.answer_id === answer_id) {
answer.correct = true
} else {
answer.correct = false
question.answers = question.answers.map((answer: Answer) => ({
...answer,
correct: answer.answer_id === answer_id ? !answer.correct : answer.correct,
}));
}
return answer
})
}
return question
})
saveQuestions(newQuestions)
return question;
});
saveQuestions(newQuestions);
}
return (
@ -308,29 +292,21 @@ function QuizBlockComponent(props: any) {
key={answer.answer_id}
className={twMerge(
'outline outline-3 pr-2 shadow w-full flex items-center space-x-2 h-[30px] bg-opacity-50 hover:bg-opacity-100 hover:shadow-md rounded-s rounded-lg bg-white text-sm hover:scale-105 active:scale-110 duration-150 cursor-pointer ease-linear',
answer.correct && isEditable
? 'outline-lime-300'
: 'outline-white',
userAnswers.find(
answer.correct && isEditable ? 'outline-lime-300' : 'outline-white',
userAnswers.some(
(userAnswer: any) =>
userAnswer.question_id === question.question_id &&
userAnswer.answer_id === answer.answer_id &&
!isEditable
)
? 'outline-slate-300'
: '',
submitted && answer.correct
? 'outline-lime-300 text-lime'
: '',
) ? 'outline-slate-300' : '',
submitted && answer.correct ? 'outline-lime-300 text-lime' : '',
submitted &&
!answer.correct &&
userAnswers.find(
userAnswers.some(
(userAnswer: any) =>
userAnswer.question_id === question.question_id &&
userAnswer.answer_id === answer.answer_id
)
? 'outline-red-400'
: ''
) ? 'outline-red-400' : ''
)}
onClick={() =>
handleAnswerClick(question.question_id, answer.answer_id)
@ -347,7 +323,7 @@ function QuizBlockComponent(props: any) {
: '',
submitted &&
!answer.correct &&
userAnswers.find(
userAnswers.some(
(userAnswer: any) =>
userAnswer.question_id === question.question_id &&
userAnswer.answer_id === answer.answer_id

View file

@ -0,0 +1,9 @@
export async function register() {
if (process.env.NEXT_RUNTIME === 'nodejs') {
await import('./sentry.server.config');
}
if (process.env.NEXT_RUNTIME === 'edge') {
await import('./sentry.edge.config');
}
}

View file

@ -37,7 +37,7 @@ export default async function middleware(req: NextRequest) {
// Out of orgslug paths & rewrite
const standard_paths = ['/home']
const auth_paths = ['/login', '/signup', '/reset']
const auth_paths = ['/login', '/signup', '/reset', '/forgot']
if (standard_paths.includes(pathname)) {
// Redirect to the same pathname with the original search params
return NextResponse.rewrite(new URL(`${pathname}${search}`, req.url))

View file

@ -17,3 +17,46 @@ const nextConfig = {
}
module.exports = nextConfig
// Injected content via Sentry wizard below
const { withSentryConfig } = require("@sentry/nextjs");
module.exports = withSentryConfig(
module.exports,
{
// For all available options, see:
// https://github.com/getsentry/sentry-webpack-plugin#options
org: "learnhouse",
project: "learnhouse-web",
// Only print logs for uploading source maps in CI
silent: !process.env.CI,
// For all available options, see:
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
// Upload a larger set of source maps for prettier stack traces (increases build time)
widenClientFileUpload: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles
hideSourceMaps: true,
// Automatically tree-shake Sentry logger statements to reduce bundle size
disableLogger: true,
// Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
// See the following for more information:
// https://docs.sentry.io/product/crons/
// https://vercel.com/docs/cron-jobs
automaticVercelMonitors: true,
}
);

View file

@ -19,19 +19,20 @@
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@sentry/nextjs": "^8.27.0",
"@stitches/react": "^1.2.8",
"@tiptap/core": "^2.5.8",
"@tiptap/extension-code-block-lowlight": "^2.5.8",
"@tiptap/extension-collaboration": "^2.5.8",
"@tiptap/extension-collaboration-cursor": "^2.5.8",
"@tiptap/extension-youtube": "^2.5.8",
"@tiptap/html": "^2.5.8",
"@tiptap/pm": "^2.5.8",
"@tiptap/react": "^2.5.8",
"@tiptap/starter-kit": "^2.5.8",
"@tiptap/core": "^2.6.6",
"@tiptap/extension-code-block-lowlight": "^2.6.6",
"@tiptap/extension-collaboration": "^2.6.6",
"@tiptap/extension-collaboration-cursor": "^2.6.6",
"@tiptap/extension-youtube": "^2.6.6",
"@tiptap/html": "^2.6.6",
"@tiptap/pm": "^2.6.6",
"@tiptap/react": "^2.6.6",
"@tiptap/starter-kit": "^2.6.6",
"@types/randomcolor": "^0.5.9",
"avvvatars-react": "^0.4.2",
"dayjs": "^1.11.12",
"dayjs": "^1.11.13",
"formik": "^2.4.6",
"framer-motion": "^10.18.0",
"get-youtube-id": "^1.0.1",
@ -39,7 +40,7 @@
"katex": "^0.16.11",
"lowlight": "^3.1.0",
"lucide-react": "^0.424.0",
"next": "14.2.5",
"next": "14.2.7",
"next-auth": "^4.24.7",
"nextjs-toploader": "^1.6.12",
"prosemirror-state": "^1.4.3",
@ -53,14 +54,14 @@
"react-katex": "^3.0.1",
"react-spinners": "^0.13.8",
"react-youtube": "^10.1.0",
"sharp": "^0.33.4",
"sharp": "^0.33.5",
"styled-components": "^6.1.12",
"swr": "^2.2.5",
"tailwind-merge": "^2.4.0",
"tailwind-merge": "^2.5.2",
"tailwind-scrollbar": "^3.1.0",
"uuid": "^9.0.1",
"y-indexeddb": "^9.0.12",
"y-prosemirror": "^1.2.11",
"y-prosemirror": "^1.2.12",
"y-webrtc": "^10.3.0",
"yjs": "^13.6.18"
},
@ -70,15 +71,15 @@
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "18.2.23",
"@types/react-katex": "^3.0.4",
"@types/react-transition-group": "^4.4.10",
"@types/react-transition-group": "^4.4.11",
"@types/styled-components": "^5.1.34",
"@types/uuid": "^9.0.8",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-config-next": "^14.2.5",
"eslint-config-next": "^14.2.7",
"eslint-plugin-unused-imports": "^3.2.0",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.10",
"typescript": "5.4.4"
}
}

2994
apps/web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,31 @@
// This file configures the initialization of Sentry on the client.
// The config you add here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 0.5,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
replaysOnErrorSampleRate: 1.0,
// This sets the sample rate to be 10%. You may want this to be 100% while
// in development and sample at a lower rate in production
replaysSessionSampleRate: 0.1,
enabled: process.env.NODE_ENV === 'development',
// You can remove this option if you're not planning to use the Sentry Session Replay feature:
integrations: [
Sentry.replayIntegration({
// Additional Replay configuration goes in here, for example:
maskAllText: true,
blockAllMedia: true,
}),
],
})

View file

@ -0,0 +1,18 @@
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 0.5,
enabled: process.env.NODE_ENV === 'development',
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
})

View file

@ -0,0 +1,20 @@
// This file configures the initialization of Sentry on the server.
// The config you add here will be used whenever the server handles a request.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from '@sentry/nextjs'
Sentry.init({
dsn: process.env.SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 0.5,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
debug: false,
enabled: process.env.NODE_ENV === 'development',
// Uncomment the line below to enable Spotlight (https://spotlightjs.com)
// spotlight: process.env.NODE_ENV === 'development',
})

View file

@ -9,8 +9,8 @@
},
"devDependencies": {
"eslint": "^8.57.0",
"prettier": "^3.3.0",
"turbo": "^1.13.3"
"prettier": "^3.3.3",
"turbo": "^1.13.4"
},
"packageManager": "pnpm@9.0.6"
}

68
pnpm-lock.yaml generated
View file

@ -12,11 +12,11 @@ importers:
specifier: ^8.57.0
version: 8.57.0
prettier:
specifier: ^3.3.0
version: 3.3.0
specifier: ^3.3.3
version: 3.3.3
turbo:
specifier: ^1.13.3
version: 1.13.3
specifier: ^1.13.4
version: 1.13.4
packages:
@ -323,8 +323,8 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier@3.3.0:
resolution: {integrity: sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g==}
prettier@3.3.3:
resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==}
engines: {node: '>=14'}
hasBin: true
@ -374,38 +374,38 @@ packages:
text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
turbo-darwin-64@1.13.3:
resolution: {integrity: sha512-glup8Qx1qEFB5jerAnXbS8WrL92OKyMmg5Hnd4PleLljAeYmx+cmmnsmLT7tpaVZIN58EAAwu8wHC6kIIqhbWA==}
turbo-darwin-64@1.13.4:
resolution: {integrity: sha512-A0eKd73R7CGnRinTiS7txkMElg+R5rKFp9HV7baDiEL4xTG1FIg/56Vm7A5RVgg8UNgG2qNnrfatJtb+dRmNdw==}
cpu: [x64]
os: [darwin]
turbo-darwin-arm64@1.13.3:
resolution: {integrity: sha512-/np2xD+f/+9qY8BVtuOQXRq5f9LehCFxamiQnwdqWm5iZmdjygC5T3uVSYuagVFsZKMvX3ycySwh8dylGTl6lg==}
turbo-darwin-arm64@1.13.4:
resolution: {integrity: sha512-eG769Q0NF6/Vyjsr3mKCnkG/eW6dKMBZk6dxWOdrHfrg6QgfkBUk0WUUujzdtVPiUIvsh4l46vQrNVd9EOtbyA==}
cpu: [arm64]
os: [darwin]
turbo-linux-64@1.13.3:
resolution: {integrity: sha512-G+HGrau54iAnbXLfl+N/PynqpDwi/uDzb6iM9hXEDG+yJnSJxaHMShhOkXYJPk9offm9prH33Khx2scXrYVW1g==}
turbo-linux-64@1.13.4:
resolution: {integrity: sha512-Bq0JphDeNw3XEi+Xb/e4xoKhs1DHN7OoLVUbTIQz+gazYjigVZvtwCvgrZI7eW9Xo1eOXM2zw2u1DGLLUfmGkQ==}
cpu: [x64]
os: [linux]
turbo-linux-arm64@1.13.3:
resolution: {integrity: sha512-qWwEl5VR02NqRyl68/3pwp3c/olZuSp+vwlwrunuoNTm6JXGLG5pTeme4zoHNnk0qn4cCX7DFrOboArlYxv0wQ==}
turbo-linux-arm64@1.13.4:
resolution: {integrity: sha512-BJcXw1DDiHO/okYbaNdcWN6szjXyHWx9d460v6fCHY65G8CyqGU3y2uUTPK89o8lq/b2C8NK0yZD+Vp0f9VoIg==}
cpu: [arm64]
os: [linux]
turbo-windows-64@1.13.3:
resolution: {integrity: sha512-Nudr4bRChfJzBPzEmpVV85VwUYRCGKecwkBFpbp2a4NtrJ3+UP1VZES653ckqCu2FRyRuS0n03v9euMbAvzH+Q==}
turbo-windows-64@1.13.4:
resolution: {integrity: sha512-OFFhXHOFLN7A78vD/dlVuuSSVEB3s9ZBj18Tm1hk3aW1HTWTuAw0ReN6ZNlVObZUHvGy8d57OAGGxf2bT3etQw==}
cpu: [x64]
os: [win32]
turbo-windows-arm64@1.13.3:
resolution: {integrity: sha512-ouJCgsVLd3icjRLmRvHQDDZnmGzT64GBupM1Y+TjtYn2LVaEBoV6hicFy8x5DUpnqdLy+YpCzRMkWlwhmkX7sQ==}
turbo-windows-arm64@1.13.4:
resolution: {integrity: sha512-u5A+VOKHswJJmJ8o8rcilBfU5U3Y1TTAfP9wX8bFh8teYF1ghP0EhtMRLjhtp6RPa+XCxHHVA2CiC3gbh5eg5g==}
cpu: [arm64]
os: [win32]
turbo@1.13.3:
resolution: {integrity: sha512-n17HJv4F4CpsYTvKzUJhLbyewbXjq1oLCi90i5tW1TiWDz16ML1eDG7wi5dHaKxzh5efIM56SITnuVbMq5dk4g==}
turbo@1.13.4:
resolution: {integrity: sha512-1q7+9UJABuBAHrcC4Sxp5lOqYS5mvxRrwa33wpIyM18hlOCpRD/fTJNxZ0vhbMcJmz15o9kkVm743mPn7p6jpQ==}
hasBin: true
type-check@0.4.0:
@ -757,7 +757,7 @@ snapshots:
prelude-ls@1.2.1: {}
prettier@3.3.0: {}
prettier@3.3.3: {}
punycode@2.3.1: {}
@ -793,32 +793,32 @@ snapshots:
text-table@0.2.0: {}
turbo-darwin-64@1.13.3:
turbo-darwin-64@1.13.4:
optional: true
turbo-darwin-arm64@1.13.3:
turbo-darwin-arm64@1.13.4:
optional: true
turbo-linux-64@1.13.3:
turbo-linux-64@1.13.4:
optional: true
turbo-linux-arm64@1.13.3:
turbo-linux-arm64@1.13.4:
optional: true
turbo-windows-64@1.13.3:
turbo-windows-64@1.13.4:
optional: true
turbo-windows-arm64@1.13.3:
turbo-windows-arm64@1.13.4:
optional: true
turbo@1.13.3:
turbo@1.13.4:
optionalDependencies:
turbo-darwin-64: 1.13.3
turbo-darwin-arm64: 1.13.3
turbo-linux-64: 1.13.3
turbo-linux-arm64: 1.13.3
turbo-windows-64: 1.13.3
turbo-windows-arm64: 1.13.3
turbo-darwin-64: 1.13.4
turbo-darwin-arm64: 1.13.4
turbo-linux-64: 1.13.4
turbo-linux-arm64: 1.13.4
turbo-windows-64: 1.13.4
turbo-windows-arm64: 1.13.4
type-check@0.4.0:
dependencies: