diff --git a/apps/api/src/app/app.module.ts b/apps/api/src/app/app.module.ts index 14ea9fb..e09278a 100644 --- a/apps/api/src/app/app.module.ts +++ b/apps/api/src/app/app.module.ts @@ -13,6 +13,7 @@ import { environment } from '../environments/environment' import { EnvironmentModule } from '../modules/environment/environment.module' import AssessmentModule from '../modules/assessment/assessment.module' import QuestionsModule from '../modules/question/question.module' +import LanguageModule from '../modules/language/language.module' function onWebsocketConnection(connectionParams: any) { return { @@ -36,6 +37,7 @@ function onWebsocketConnection(connectionParams: any) { EnvironmentModule, AssessmentModule, QuestionsModule, + LanguageModule, // Main GraphQL module GraphQLModule.forRoot({ diff --git a/apps/api/src/modules/assessment/assessment.module.ts b/apps/api/src/modules/assessment/assessment.module.ts index ecd3cc8..94eb0fd 100644 --- a/apps/api/src/modules/assessment/assessment.module.ts +++ b/apps/api/src/modules/assessment/assessment.module.ts @@ -1,10 +1,11 @@ import { ModelsModule } from '@aelp-app/models' import { Module } from '@nestjs/common' +import QuestionsModule from '../question/question.module' import AssessmentResolver from './assessment.resolver' import AssessmentService from './assessment.service' @Module({ - imports: [ModelsModule], + imports: [ModelsModule, QuestionsModule], providers: [AssessmentService, AssessmentResolver], }) export default class AssessmentModule {} diff --git a/apps/api/src/modules/assessment/assessment.service.ts b/apps/api/src/modules/assessment/assessment.service.ts index 089576a..47f7954 100644 --- a/apps/api/src/modules/assessment/assessment.service.ts +++ b/apps/api/src/modules/assessment/assessment.service.ts @@ -1,15 +1,19 @@ import { PrismaService } from '@aelp-app/models' import { Injectable } from '@nestjs/common' import { UserInputError } from 'apollo-server-express' -import { ClassroomRole } from '../../global-types' +import { ClassroomRole, QuestionType } from '../../global-types' +import QuestionService from '../question/question.service' import { User } from '../user/types/user.model' import { AssessmentCreateInput } from './types/assessment-create.input' @Injectable() export default class AssessmentService { - constructor(private prismaService: PrismaService) {} + constructor( + private prismaService: PrismaService, + private questionService: QuestionService + ) {} async createAssessment(data: AssessmentCreateInput, user: User) { - const { classroomId, ...rest } = data + const { classroomId, questions, ...rest } = data if (classroomId) { const classroom = await this.prismaService.classroom.findUnique({ @@ -29,12 +33,50 @@ export default class AssessmentService { } } - return this.prismaService.assessment.create({ - data: { - ...rest, - user: { connect: { id: user.id } }, - classroom: classroomId ? { connect: { id: classroomId } } : undefined, - }, + return this.prismaService.$transaction(async prisma => { + const questionsTransformed = questions.map( + this.questionService.transformQuestionToCreateQuery + ) + + const questionsIds = [] + for (const question of questionsTransformed) { + const { programmingQuestion, multipleChoiceQuestion, ...rest } = + question + + const createdQuestion = await prisma.question.create({ + data: { ...rest }, + }) + + if (programmingQuestion) { + await prisma.programmingQuestion.create({ + data: { + + ...programmingQuestion, + baseQuestion: { connect: { id: createdQuestion.id } }, + }, + }) + } else if (multipleChoiceQuestion) { + await prisma.multipleChoiceQuestion.create({ + data: { + ...multipleChoiceQuestion, + baseQuestion: { connect: { id: createdQuestion.id } }, + }, + }) + } + + questionsIds.push(createdQuestion.id) + } + + return prisma.assessment.create({ + data: { + ...rest, + questions: { + connect: questionsIds.map(id => ({ id })), + }, + user: { connect: { id: user.id } }, + classroom: classroomId ? { connect: { id: classroomId } } : undefined, + }, + }) }) } } diff --git a/apps/api/src/modules/assessment/types/assessment-create.input.ts b/apps/api/src/modules/assessment/types/assessment-create.input.ts index 231ae34..4272f19 100644 --- a/apps/api/src/modules/assessment/types/assessment-create.input.ts +++ b/apps/api/src/modules/assessment/types/assessment-create.input.ts @@ -2,6 +2,7 @@ import { Field } from '@nestjs/graphql' import { InputType } from '@nestjs/graphql' import { Int } from '@nestjs/graphql' import { AssessmentType } from '../../../global-types' +import { QuestionCreateInput } from '../../question/types/question-create-input' @InputType() export class AssessmentCreateInput { @@ -12,7 +13,10 @@ export class AssessmentCreateInput { description!: string @Field(() => AssessmentType, { nullable: false }) - assessmentType!: keyof typeof AssessmentType + assessmentType!: AssessmentType + + @Field(() => [QuestionCreateInput], { nullable: false }) + questions!: QuestionCreateInput[] @Field(() => Date, { nullable: true }) startTime?: Date | string diff --git a/apps/api/src/modules/environment/environment.service.ts b/apps/api/src/modules/environment/environment.service.ts index c3315ee..e262f5e 100644 --- a/apps/api/src/modules/environment/environment.service.ts +++ b/apps/api/src/modules/environment/environment.service.ts @@ -163,9 +163,11 @@ export class EnvironmentService { } const defaultLanguageId = - programmingQuestion.singleFileProgrammingQuestion?.defaultCodes[0]?.id || + programmingQuestion.singleFileProgrammingQuestion?.defaultCodes[0]?.languageId || programmingQuestion.multipleFilesProgrammingQuestion.languageId + if (!defaultLanguageId) throw new Error('No default language') + const answer = this.prismaService.questionAnswer.create({ data: { question: { diff --git a/apps/api/src/modules/evaluation-criteria/types/evaluation-criteria-create-input.ts b/apps/api/src/modules/evaluation-criteria/types/evaluation-criteria-create-input.ts new file mode 100644 index 0000000..cf18909 --- /dev/null +++ b/apps/api/src/modules/evaluation-criteria/types/evaluation-criteria-create-input.ts @@ -0,0 +1,18 @@ +import { Field, InputType, Int } from '@nestjs/graphql' +import { EvaluationCriteriaType } from '../../../global-types' +import { InputOutputEvaluationCriteriaCreateInput } from './input-output-evaluation-criteria-create-input' + +@InputType() +export class EvaluationCriteriaCreateInput { + @Field(() => String, { nullable: false }) + name!: string + + @Field(() => EvaluationCriteriaType, { nullable: false }) + type!: EvaluationCriteriaType + + @Field(() => Int, { nullable: false }) + totalPoints!: number + + @Field(() => InputOutputEvaluationCriteriaCreateInput, { nullable: true }) + inputOutputEvaluationCriteria?: InputOutputEvaluationCriteriaCreateInput +} diff --git a/apps/api/src/modules/evaluation-criteria/types/input-output-evaluation-criteria-count.output.ts b/apps/api/src/modules/evaluation-criteria/types/input-output-evaluation-criteria-count.output.ts new file mode 100644 index 0000000..bfb338e --- /dev/null +++ b/apps/api/src/modules/evaluation-criteria/types/input-output-evaluation-criteria-count.output.ts @@ -0,0 +1,10 @@ +import { Field } from '@nestjs/graphql'; +import { ObjectType } from '@nestjs/graphql'; +import { Int } from '@nestjs/graphql'; + +@ObjectType() +export class InputOutputEvaluationCriteriaCount { + + @Field(() => Int, {nullable:false}) + baseCriteria!: number; +} diff --git a/apps/api/src/modules/evaluation-criteria/types/input-output-evaluation-criteria-create-input.ts b/apps/api/src/modules/evaluation-criteria/types/input-output-evaluation-criteria-create-input.ts new file mode 100644 index 0000000..61bbfec --- /dev/null +++ b/apps/api/src/modules/evaluation-criteria/types/input-output-evaluation-criteria-create-input.ts @@ -0,0 +1,14 @@ +import { Field } from '@nestjs/graphql' +import { InputType } from '@nestjs/graphql' +@InputType() +export class InputOutputEvaluationCriteriaCreateInput { + @Field(() => [String], { + nullable: true, + }) + inputs?: string[] + + @Field(() => [String], { + nullable: true, + }) + outputs?: string[] +} diff --git a/apps/api/src/modules/evaluation-criteria/types/input-output-evaluation-criteria.model.ts b/apps/api/src/modules/evaluation-criteria/types/input-output-evaluation-criteria.model.ts new file mode 100644 index 0000000..ed2be42 --- /dev/null +++ b/apps/api/src/modules/evaluation-criteria/types/input-output-evaluation-criteria.model.ts @@ -0,0 +1,30 @@ +import { Field } from '@nestjs/graphql'; +import { ObjectType } from '@nestjs/graphql'; +import { ID } from '@nestjs/graphql'; +import { EvaluationCriteria } from './evaluation-criteria.model'; +import { InputOutputEvaluationCriteriaCount } from './input-output-evaluation-criteria-count.output'; + +@ObjectType() +export class InputOutputEvaluationCriteria { + + @Field(() => ID, {nullable:false}) + id!: string; + + @Field(() => [String], {nullable:true}) + inputs!: Array; + + @Field(() => [String], {nullable:true}) + outputs!: Array; + + @Field(() => [EvaluationCriteria], {nullable:true}) + baseCriteria?: Array; + + @Field(() => Date, {nullable:false}) + updatedAt!: Date; + + @Field(() => Date, {nullable:false}) + createdAt!: Date; + + @Field(() => InputOutputEvaluationCriteriaCount, {nullable:false}) + _count?: InputOutputEvaluationCriteriaCount; +} diff --git a/apps/api/src/modules/language/language.module.ts b/apps/api/src/modules/language/language.module.ts new file mode 100644 index 0000000..59cd5c1 --- /dev/null +++ b/apps/api/src/modules/language/language.module.ts @@ -0,0 +1,6 @@ +import { ModelsModule } from '@aelp-app/models' +import { Module } from '@nestjs/common' +import LanguageResolver from './language.resolver' + +@Module({ imports: [ModelsModule], providers: [LanguageResolver] }) +export default class LanguageModule {} diff --git a/apps/api/src/modules/language/language.resolver.ts b/apps/api/src/modules/language/language.resolver.ts new file mode 100644 index 0000000..59175ed --- /dev/null +++ b/apps/api/src/modules/language/language.resolver.ts @@ -0,0 +1,14 @@ +import { PrismaService } from '@aelp-app/models' +import { Query } from '@nestjs/graphql' +import { Resolver } from '@nestjs/graphql' +import { Language } from './types/language.model' + +@Resolver(() => Language) +export default class LanguageResolver { + constructor(private prismaService: PrismaService) {} + + @Query(() => [Language]) + async languages() { + return this.prismaService.language.findMany() + } +} diff --git a/apps/api/src/modules/language/types/language.model.ts b/apps/api/src/modules/language/types/language.model.ts index 5909d59..b00baba 100644 --- a/apps/api/src/modules/language/types/language.model.ts +++ b/apps/api/src/modules/language/types/language.model.ts @@ -1,10 +1,12 @@ import { Field } from '@nestjs/graphql' import { ObjectType } from '@nestjs/graphql' import { ID } from '@nestjs/graphql' +import { Environment } from '../../environment/types/environment.model' +import { File } from '../../environment/types/file.model' import { MultipleFilesProgrammingQuestion } from '../../question/types/multiple-files-programming-question.model' -import { SingleFileProgrammingQuestion } from '../../question/types/single-file-programming-question.model' import { LanguageCount } from './language-count.output' + @ObjectType() export class Language { @Field(() => ID, { nullable: false }) @@ -16,21 +18,39 @@ export class Language { @Field(() => String, { nullable: false }) extension!: string - @Field(() => String, { nullable: false }) - editorConfigName!: string - - @Field(() => [SingleFileProgrammingQuestion], { nullable: true }) - singleFileQuestions?: Array - @Field(() => Date, { nullable: false }) createdAt!: Date @Field(() => Date, { nullable: false }) updatedAt!: Date + @Field(() => String, { nullable: false, defaultValue: 'main.${extension}' }) + defaultFileName!: string + + @Field(() => String, { nullable: false, defaultValue: '${extension}' }) + editorConfigName!: string + + @Field(() => String, { nullable: false, defaultValue: '' }) + defaultCode!: string + + @Field(() => String, { nullable: false, defaultValue: '' }) + compilerPackageName!: string + + @Field(() => String, { nullable: false, defaultValue: '' }) + version!: string + + @Field(() => [File], { nullable: true }) + File?: Array + + @Field(() => [Environment], { nullable: true }) + Environment?: Array + @Field(() => [MultipleFilesProgrammingQuestion], { nullable: true }) MultipleFilesProgrammingQuestion?: Array + // @Field(() => [SingleFileProgrammingQuestionDefaultCode], { nullable: true }) + // SingleFileProgrammingQuestionDefaultCode?: Array + @Field(() => LanguageCount, { nullable: false }) _count?: LanguageCount } diff --git a/apps/api/src/modules/question/question.module.ts b/apps/api/src/modules/question/question.module.ts index 45fff81..f863df7 100644 --- a/apps/api/src/modules/question/question.module.ts +++ b/apps/api/src/modules/question/question.module.ts @@ -14,7 +14,8 @@ import { QuestionAnswer } from './types/question-answer.model' QuestionResolver, ProgrammingQuestionAnswerResolver, QuestionAnswerResolver, - QuestionAnswerService + QuestionAnswerService, ], + exports: [QuestionService], }) export default class QuestionsModule {} diff --git a/apps/api/src/modules/question/question.resolver.ts b/apps/api/src/modules/question/question.resolver.ts index 4cfc551..ac2e491 100644 --- a/apps/api/src/modules/question/question.resolver.ts +++ b/apps/api/src/modules/question/question.resolver.ts @@ -1,22 +1,20 @@ -import { Args, Mutation, ResolveField, Resolver, Root } from '@nestjs/graphql' -import { LoggedInUser } from '../../utils/decorators/LoggedInUser' -import { User } from '../user/types/user.model' +import { ResolveField, Resolver, Root } from '@nestjs/graphql' import QuestionService from './question.service' +import { MultipleChoiceQuestion } from './types/multiple-choice-question.model' import { ProgrammingQuestion } from './types/programming-question.model' -import { QuestionCreateInput } from './types/question-create-input' import { Question } from './types/question.model' @Resolver(() => Question) export default class QuestionResolver { constructor(private questionService: QuestionService) {} - @Mutation(() => Question) - async createQuestion(@Args('data') data: QuestionCreateInput, @LoggedInUser() user: User) { - return this.questionService.createQuestion(data, user) - } - @ResolveField(() => ProgrammingQuestion) async programmingQuestion(@Root() question: Question) { return this.questionService.getById(question.id).programmingQuestion() } + + @ResolveField(() => MultipleChoiceQuestion) + async multipleChoiceQuestion(@Root() question: Question) { + return this.questionService.getById(question.id).multipleChoiceQuestion() + } } diff --git a/apps/api/src/modules/question/question.service.ts b/apps/api/src/modules/question/question.service.ts index 4509f9b..32aefe5 100644 --- a/apps/api/src/modules/question/question.service.ts +++ b/apps/api/src/modules/question/question.service.ts @@ -2,7 +2,6 @@ import { PrismaService } from '@aelp-app/models' import { Injectable } from '@nestjs/common' import { Prisma, Question, QuestionType } from '@aelp-app/models' import { UserInputError } from 'apollo-server-express' -import { User } from '../user/types/user.model' import { QuestionCreateInput } from './types/question-create-input' @Injectable() export default class QuestionService { @@ -14,12 +13,16 @@ export default class QuestionService { }) } - async createQuestion(data: QuestionCreateInput, user: User) { + transformQuestionToCreateQuery(data: QuestionCreateInput): { + questionType: QuestionType + points: number + multipleChoiceQuestion?: Prisma.MultipleChoiceQuestionCreateWithoutBaseQuestionInput + programmingQuestion?: Prisma.ProgrammingQuestionCreateWithoutBaseQuestionInput + } { const { multipleChoiceQuestion, programmingQuestion, questionType, - assessmentId, points, } = data @@ -34,63 +37,45 @@ export default class QuestionService { throw new UserInputError('Programming question is missing') } - const assessment = await this.prismaService.assessment.findUnique({ - where: { id: assessmentId }, - }) - - if (!assessment || assessment.userId !== user.id) { - throw new UserInputError('Assessment not found') - } - const commonData = { questionType, points, - assessment: { connect: { id: assessment.id } }, } if (questionType === QuestionType.MULTIPLE_CHOICE) { - return this.prismaService.question.create({ - data: { - ...commonData, - multipleChoiceQuestion: { - create: { - description: multipleChoiceQuestion.description, - choices: { - createMany: { - data: multipleChoiceQuestion.choices.map((choice, index) => ({ - choice: choice, - correct: - multipleChoiceQuestion.correctChoiceIndex === index, - })), - }, - }, + return { + ...commonData, + multipleChoiceQuestion: { + description: multipleChoiceQuestion.description, + choices: { + createMany: { + data: multipleChoiceQuestion.choices.map((choice, index) => ({ + choice: choice, + correct: multipleChoiceQuestion.correctChoiceIndex === index, + })), }, }, }, - }) + } } else if (questionType === QuestionType.PROGRAMMING) { - return this.prismaService.question.create({ - data: { - ...commonData, - programmingQuestion: { + return { + ...commonData, + programmingQuestion: { + programmingQuestionType: programmingQuestion.programmingQuestionType, + statementMrkdwn: programmingQuestion.statementMrkdwn, + title: programmingQuestion.title, + singleFileProgrammingQuestion: { create: { - programmingQuestionType: - programmingQuestion.programmingQuestionType, - statementMrkdwn: programmingQuestion.statementMrkdwn, - title: programmingQuestion.title, - singleFileProgrammingQuestion: { - create: { - defaultCodes: { - createMany: { data: [] }, // TODO: Fix this - }, + defaultCodes: { + createMany: { + data: programmingQuestion.singleFileProgrammingQuestion + .defaultCodes, }, }, }, }, }, - }) + } } - - // const question = {} as (typeof multipleChoiceQuestion & typeof programmingQuestion) } } diff --git a/apps/api/src/modules/question/types/question-create-input.ts b/apps/api/src/modules/question/types/question-create-input.ts index c32af3b..fc3f5e9 100644 --- a/apps/api/src/modules/question/types/question-create-input.ts +++ b/apps/api/src/modules/question/types/question-create-input.ts @@ -1,5 +1,6 @@ import { Field, InputType, Int } from '@nestjs/graphql' import { ProgrammingQuestionType, QuestionType } from '../../../global-types' +import { EvaluationCriteriaCreateInput } from '../../evaluation-criteria/types/evaluation-criteria-create-input' @InputType() export class MultipleChoiceQuestionInput { @@ -15,15 +16,24 @@ export class MultipleChoiceQuestionInput { @InputType() export class SingleFileProgrammingQuestionInput { + @Field(() => [DefaultCodeInput], { nullable: false }) + defaultCodes?: DefaultCodeInput[] +} + +@InputType() +export class DefaultCodeInput { @Field(() => String, { nullable: false }) - defaultCode?: string + defaultCode!: string + + @Field(() => String, { nullable: false }) + languageId!: string } // @InputType() // export class MultipleFileProgrammingQuestionInput {} @InputType() export class ProgrammingQuestionInput { - @Field(() => ProgrammingQuestionType, { nullable: true }) + @Field(() => ProgrammingQuestionType, { nullable: false }) programmingQuestionType!: ProgrammingQuestionType @Field(() => SingleFileProgrammingQuestionInput, { nullable: true }) @@ -32,6 +42,9 @@ export class ProgrammingQuestionInput { // @Field(() => MultipleFileProgrammingQuestionInput, { nullable: true }) // multipleFileProgrammingQuestion?: MultipleFileProgrammingQuestionInput + @Field(() => [EvaluationCriteriaCreateInput], { nullable: false }) + evaluationCriteria?: EvaluationCriteriaCreateInput[] + @Field(() => String, { nullable: false }) title!: string @@ -54,7 +67,4 @@ export class QuestionCreateInput { @Field(() => ProgrammingQuestionInput, { nullable: true }) programmingQuestion?: ProgrammingQuestionInput - - @Field(() => String, { nullable: true }) - assessmentId?: string } diff --git a/apps/frontend/components/molecules/advanced-text-editor/advanced-text-editor.tsx b/apps/frontend/components/molecules/advanced-text-editor/advanced-text-editor.tsx index 702751a..40c9e74 100644 --- a/apps/frontend/components/molecules/advanced-text-editor/advanced-text-editor.tsx +++ b/apps/frontend/components/molecules/advanced-text-editor/advanced-text-editor.tsx @@ -6,6 +6,7 @@ import type { MDEditorProps } from '@uiw/react-md-editor' import rehypeSanitize from 'rehype-sanitize' import rehypeKatex from 'rehype-katex' import Loader from '@components/primitives/loader' +import { Path, useController, useFormContext } from 'react-hook-form' const MDEditor = dynamic( () => import('@uiw/react-md-editor').then(mod => mod.default), @@ -20,6 +21,7 @@ const MDEditor = dynamic( ) as React.ComponentType export interface TextEditorProps { + name?: string expanded?: boolean value: string onChange: (newValue: string) => void @@ -79,3 +81,24 @@ export default function AdvancedTextEditor({ ) } + +export function HFAdvancedTextEditor({ + name, + ...props +}: Omit & { name: Path }) { + const { control } = useFormContext() + const { + field: { name: newName, onBlur, onChange, value }, + } = useController({ control, name }) + + return ( + + ) +} diff --git a/apps/frontend/components/primitives/button.tsx b/apps/frontend/components/primitives/button.tsx index fbdb73b..7d980f2 100644 --- a/apps/frontend/components/primitives/button.tsx +++ b/apps/frontend/components/primitives/button.tsx @@ -81,7 +81,7 @@ export default function Button({ )} {...rest} > - {icon || rightIcon ? ( + {(icon || rightIcon) ? (
{icon &&
{icon}
} {modifiedChildren && ( @@ -90,7 +90,7 @@ export default function Button({ )} {rightIcon && ( -
{icon}
+
{rightIcon}
)}
) : ( diff --git a/apps/frontend/components/primitives/checkbox/checkbox.tsx b/apps/frontend/components/primitives/checkbox/checkbox.tsx index d47edbf..710bd9c 100644 --- a/apps/frontend/components/primitives/checkbox/checkbox.tsx +++ b/apps/frontend/components/primitives/checkbox/checkbox.tsx @@ -21,6 +21,7 @@ const Checkbox = React.forwardRef( children, invalid = false, invalidText, + className, ...rest }: CheckboxProps) => { return ( @@ -33,7 +34,8 @@ const Checkbox = React.forwardRef( 'border-2 w-[1.25rem] h-[1.25rem] my-1 rounded overflow-hidden focus:ring-4 transition-shadow ring-accent ring-opacity-25', checked && 'bg-accent', checked && 'border-accent', - invalid && 'border-error' + invalid && 'border-error', + className, )} > @@ -46,7 +48,7 @@ const Checkbox = React.forwardRef( -
{children}
+
{children}
{invalid && invalidText && (
{invalidText}
@@ -60,9 +62,10 @@ Checkbox.displayName = 'Checkbox' export type HFCheckboxProps = Omit< CheckboxProps, - 'checked' | 'onCheckedChange' | 'invalid' | 'invalidText' + 'checked' | 'onCheckedChange' | 'invalid' | 'invalidText' | 'name' > & { rules?: UseControllerProps['rules'] + name: Path } export function HFCheckbox({ @@ -86,8 +89,8 @@ export function HFCheckbox({ onBlur={onBlur} checked={value as boolean} onCheckedChange={onChange} - invalid={errors[name]?.message} - invalidText={errors[name]?.message} + invalid={errors[name as string]?.message} + invalidText={errors[name as string]?.message} {...rest} > {children} diff --git a/apps/frontend/components/primitives/code-editor/code-editor.tsx b/apps/frontend/components/primitives/code-editor/code-editor.tsx new file mode 100644 index 0000000..2b8a6de --- /dev/null +++ b/apps/frontend/components/primitives/code-editor/code-editor.tsx @@ -0,0 +1,52 @@ +import Editor, { EditorProps, useMonaco } from '@monaco-editor/react' +import { useEffect } from 'react' +import { Path, useController, useFormContext } from 'react-hook-form' + +const theme = { + base: 'vs', + inherit: true, + rules: [ + { + foreground: '04B46D', + fontStyle: 'bold', + token: 'keyword', + }, + ], +} + +export default function CodeEditor(props: EditorProps) { + const monaco = useMonaco() + + useEffect(() => { + if (monaco) { + monaco.editor.defineTheme('aelpTheme', theme as any) + monaco.editor.setTheme('aelpTheme') + } + }, [monaco]) + + return ( + + ) +} + +export function HFCodeEditor({ name, ...rest}: { name: Path } & Omit) { + const { control } = useFormContext() + + const { + field: { onChange, value }, + } = useController({ control, name }) + + return +} diff --git a/apps/frontend/components/primitives/date-picker/calender.component.tsx b/apps/frontend/components/primitives/date-picker/calender.component.tsx new file mode 100644 index 0000000..09cd52a --- /dev/null +++ b/apps/frontend/components/primitives/date-picker/calender.component.tsx @@ -0,0 +1,100 @@ +import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/outline' +import classNames from 'classnames' +import { RenderProps } from 'dayzed' +import Button from '../button' +import IconButton from '../icon-button/icon-button' +import Input from '../input/input' + +const weekdayNamesShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] +const monthNamesShort = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +] + +export default function Calendar({ + calendars, + getBackProps, + getForwardProps, + getDateProps, +}: RenderProps) { + if (calendars.length) { + const calendar = calendars[0] + + return ( +
+
+ } + {...getBackProps({ calendars })} + /> +
+ {monthNamesShort[calendar.month]} {calendar.year} +
+ } + {...getForwardProps({ calendars })} + /> +
+
+
+ {weekdayNamesShort.map(weekday => ( +
+ {weekday} +
+ ))} +
+ {calendar.weeks.map((week, weekIndex) => ( +
+ {week.map((dateObj, index) => { + const key = `${calendar.month}${calendar.year}${weekIndex}${index}` + + if (!dateObj) { + return
+ } + + const { date, selected, selectable, today } = dateObj + + let backgroundClass = today + ? 'bg-accent bg-opacity-20 text-accent' + : '' + backgroundClass = selected + ? 'bg-accent text-white font-bold' + : backgroundClass + + return ( + + ) + })} +
+ ))} +
+
+ ) + } + return null +} diff --git a/apps/frontend/components/primitives/date-picker/date-picker.component.tsx b/apps/frontend/components/primitives/date-picker/date-picker.component.tsx new file mode 100644 index 0000000..47599e1 --- /dev/null +++ b/apps/frontend/components/primitives/date-picker/date-picker.component.tsx @@ -0,0 +1,36 @@ +import Dayzed, { Props } from 'dayzed' +import { HUMAN_DATE_ONLY_FORMAT } from 'lib/utils/date' +import moment from 'moment' +import { useState } from 'react' +import Input from '../input/input' +import Calendar from './calender.component' + +type DatePickerProps = Omit + +export default function DatePicker({ + onDateSelected, + ...props +}: DatePickerProps) { + const [show, setShow] = useState(false) + + return ( +
setShow(true)} + > + + {show && ( + { + setShow(false) + return onDateSelected(date, event) + }} + render={dayzedData => } + /> + )} +
+ ) +} diff --git a/apps/frontend/components/primitives/form/form.tsx b/apps/frontend/components/primitives/form/form.tsx index 7b050ed..d55b89a 100644 --- a/apps/frontend/components/primitives/form/form.tsx +++ b/apps/frontend/components/primitives/form/form.tsx @@ -1,17 +1,20 @@ -import React, { ReactElement } from 'react'; +import React from 'react' import { FormProvider, UnpackNestedValue, useForm, UseFormProps, -} from 'react-hook-form'; -import './form.module.scss'; + UseFormReturn, +} from 'react-hook-form' +import './form.module.scss' export interface FormProps extends Omit, 'onSubmit'> { - children?: React.ReactNode; - hfOptions?: UseFormProps; - onSubmit?: (data: UnpackNestedValue) => void; + children?: + | React.ReactNode + | ((methods: UseFormReturn) => React.ReactNode) + hfOptions?: UseFormProps + onSubmit?: (data: UnpackNestedValue) => void } export function HForm({ @@ -20,15 +23,15 @@ export function HForm({ children, ...rest }: FormProps) { - const methods = useForm(hfOptions); + const methods = useForm(hfOptions) return (
- {children} + {typeof children === 'function' ? children(methods) : children}
- ); + ) } -export default HForm; +export default HForm diff --git a/apps/frontend/components/primitives/input/input.tsx b/apps/frontend/components/primitives/input/input.tsx index 4998704..1d36af6 100644 --- a/apps/frontend/components/primitives/input/input.tsx +++ b/apps/frontend/components/primitives/input/input.tsx @@ -1,5 +1,5 @@ import classNames from 'classnames' -import React, { LegacyRef } from 'react' +import React, { LegacyRef, useEffect } from 'react' import { Path, useFormContext } from 'react-hook-form' export interface InputProps @@ -51,7 +51,7 @@ export interface HFInputProps extends Omit { name: Path } -export function HFInput({ name, ...rest }: HFInputProps) { +export function HFInput({ name, type, ...rest }: HFInputProps) { const { register, formState: { errors }, @@ -59,7 +59,8 @@ export function HFInput({ name, ...rest }: HFInputProps) { return ( void; + size?: Size title?: string; children?: React.ReactNode; } +const SIZE_MAP: Record = { + xs: 'w-48', + sm: 'w-96', + md: 'w-[30rem]', + lg: 'w-[48rem]', + xl: 'w-[64rem]', +} + export function Modal(props: ModalProps) { + const size = props.size || 'md'; + return ( -
-
+
+
{props.title} diff --git a/apps/frontend/components/primitives/popover-menu/popover-menu.tsx b/apps/frontend/components/primitives/popover-menu/popover-menu.tsx index ebe51ec..2ab7a03 100644 --- a/apps/frontend/components/primitives/popover-menu/popover-menu.tsx +++ b/apps/frontend/components/primitives/popover-menu/popover-menu.tsx @@ -5,7 +5,7 @@ import { DOMAttributes, Fragment } from 'react' function PopoverMenu({ children }: { children: React.ReactNode }) { return ( - + } /> diff --git a/apps/frontend/components/primitives/select/select.component.tsx b/apps/frontend/components/primitives/select/select.component.tsx index 70d9dd0..b9bdb52 100644 --- a/apps/frontend/components/primitives/select/select.component.tsx +++ b/apps/frontend/components/primitives/select/select.component.tsx @@ -2,10 +2,12 @@ import { Listbox, Transition } from '@headlessui/react' import { CheckIcon, SelectorIcon } from '@heroicons/react/outline' import classNames from 'classnames' import React, { Fragment } from 'react' +import { Path, useController, useFormContext } from 'react-hook-form' interface SelectProps { children?: React.ReactNode className?: string + name?: string value: T options: Option[] onChange: (v: T) => void @@ -21,8 +23,7 @@ function Select({ options, className, ...props }: SelectProps) {
- {options.find(option => option.value === props.value)?.label || - 'Select'} + {options.find(option => option.value === props.value)?.label} {options.map(option => ( @@ -36,6 +37,21 @@ function Select({ options, className, ...props }: SelectProps) { ) } +export function HFSelect({ + name: prevName, + ...props +}: Omit, 'onChange' | 'value' | 'name'> & { name: Path }) { + const { control } = useFormContext() + const { + field: { name, onChange, value }, + } = useController({ + name: prevName as Path, + control, + }) + + return