From 1285a64b15951e3ddafb16e3859dfe49cba761f7 Mon Sep 17 00:00:00 2001 From: Domenico Gemoli Date: Wed, 17 Jan 2024 14:42:26 +0100 Subject: [PATCH] Lesson plan WIP --- prisma/schema.prisma | 131 +++-- src/components/LessonPlan.tsx | 44 ++ src/components/LessonPlanEditForm.tsx | 552 ++++++++++++++++++ src/components/MultiSelectDropdown.tsx | 105 ++-- src/components/PageLayout.tsx | 38 +- src/components/ResourceEditForm.tsx | 5 +- src/pages/lesson-plan/[lessonPlanId].tsx | 172 ++++++ src/pages/lesson-plan/[lessonPlanId]/edit.tsx | 115 ++++ src/pages/lesson-plan/create.tsx | 50 ++ src/pages/resource/[id]/edit.tsx | 4 +- src/pages/{ => resource}/create.tsx | 4 +- src/server/api/root.ts | 2 + src/server/api/routers/lessonPlan.ts | 255 ++++++++ src/utils/zod.ts | 33 ++ 14 files changed, 1392 insertions(+), 118 deletions(-) create mode 100644 src/components/LessonPlan.tsx create mode 100644 src/components/LessonPlanEditForm.tsx create mode 100644 src/pages/lesson-plan/[lessonPlanId].tsx create mode 100644 src/pages/lesson-plan/[lessonPlanId]/edit.tsx create mode 100644 src/pages/lesson-plan/create.tsx rename src/pages/{ => resource}/create.tsx (95%) create mode 100644 src/server/api/routers/lessonPlan.ts diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 83d1b4a..1605560 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -49,11 +49,13 @@ model Resource { relatedResources Resource[] @relation("RelatedResources") // child Resource (Resource that are suggested) relatedResourceParent Resource[] @relation("RelatedResources") // parent Resource (parent Resource of a suggested Resource) - published Boolean @default(false) - createdBy User @relation(fields: [createdById], references: [id]) - createdById String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + published Boolean @default(false) + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + lessonPlanItems LessonPlanItem[] @@index([createdById]) } @@ -69,58 +71,103 @@ model CategoriesOnResources { @@index([resourceId]) } +model LessonPlanItem { + id String @id @default(cuid()) + + text String? @db.Text + + resource Resource? @relation(fields: [resourceId], references: [id], onUpdate: Cascade, onDelete: Cascade) + resourceId String? // relation scalar field (used in the `@relation` attribute above) + duration Int? + + order Int + + section LessonPlanSection @relation(fields: [sectionId], references: [id], onUpdate: Cascade, onDelete: Cascade) + sectionId String + + @@index([sectionId]) + @@index([resourceId]) +} + +model LessonPlanSection { + id String @id @default(cuid()) + title String? + items LessonPlanItem[] + lessonPlan LessonPlan @relation(fields: [lessonPlanId], references: [id], onUpdate: Cascade, onDelete: Cascade) + lessonPlanId String + order Int + + @@index([lessonPlanId]) +} + +model LessonPlan { + id String @id @default(cuid()) + title String + description String? @db.Text + private Boolean @default(true) + useDuration Boolean @default(true) + createdBy User @relation(fields: [createdById], references: [id]) + createdById String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sections LessonPlanSection[] + + @@index([createdById]) +} // Necessary for Next auth model Account { - id String @id @default(cuid()) - userId String - type String - provider String - providerAccountId String - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) - @@index([userId]) + id String @id @default(cuid()) + userId String + type String + provider String + providerAccountId String + refresh_token String? @db.Text + access_token String? @db.Text + expires_at Int? + token_type String? + scope String? + id_token String? @db.Text + session_state String? + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([provider, providerAccountId]) + @@index([userId]) } model Session { - id String @id @default(cuid()) - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + id String @id @default(cuid()) + sessionToken String @unique + userId String + expires DateTime + user User @relation(fields: [userId], references: [id], onDelete: Cascade) - @@index([userId]) + @@index([userId]) } enum UserRole { - ADMIN - USER + ADMIN + USER } model User { - id String @id @default(cuid()) - name String? - email String? @unique - emailVerified DateTime? - image String? - role UserRole? @default(USER) - accounts Account[] - sessions Session[] - resources Resource[] + id String @id @default(cuid()) + name String? + email String? @unique + emailVerified DateTime? + image String? + role UserRole? @default(USER) + accounts Account[] + sessions Session[] + resources Resource[] + lessonPlans LessonPlan[] } model VerificationToken { - identifier String - token String @unique - expires DateTime + identifier String + token String @unique + expires DateTime - @@unique([identifier, token]) + @@unique([identifier, token]) } diff --git a/src/components/LessonPlan.tsx b/src/components/LessonPlan.tsx new file mode 100644 index 0000000..3cf6060 --- /dev/null +++ b/src/components/LessonPlan.tsx @@ -0,0 +1,44 @@ +import ReactMarkdown from "react-markdown"; +import Link from "next/link"; +import type * as z from "zod"; + +import type { RouterOutputs } from "~/utils/api"; +import type { ResourceConfiguation, ResourceType } from "@prisma/client"; +import { use, useMemo } from "react"; +import { lessonPlanCreateSchema, type resourceCreateSchema } from "~/utils/zod"; +import { ScrollArea } from "~/components/ui/scroll-area"; +import { Separator } from "./ui/separator"; + +type ApiLessonPlan = Readonly; +type CreationLessonPlan = z.infer; +type LessonPlanUnion = ApiLessonPlan | CreationLessonPlan; + +type Props = { + lessonPlan: LessonPlanUnion; +}; +export function SingleLessonPlanComponent({ lessonPlan }: Props) { + const isPreviewItem = ( + item: LessonPlanUnion["sections"][0]["items"][0], + ): item is CreationLessonPlan["sections"][0]["items"][0] => { + return !!item.resource && "label" in item.resource; + }; + + return ( +
+

{lessonPlan.description}

+ {lessonPlan.sections.map((section, index) => ( +
+

{section.title}

+ {section.items.map((item, itemIndex) => ( +
+ {isPreviewItem(item) + ? item.resource?.label + : item.resource?.title} + - {item.text} +
+ ))} +
+ ))} +
+ ); +} diff --git a/src/components/LessonPlanEditForm.tsx b/src/components/LessonPlanEditForm.tsx new file mode 100644 index 0000000..cd11cd8 --- /dev/null +++ b/src/components/LessonPlanEditForm.tsx @@ -0,0 +1,552 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { type UseFormReturn, useForm, useFieldArray } from "react-hook-form"; +import type * as z from "zod"; +import { useToast } from "~/components/ui/use-toast"; + +import { type RouterOutputs, api } from "~/utils/api"; +import { lessonPlanCreateSchema } from "~/utils/zod"; +import { MultiSelectDropown } from "~/components/MultiSelectDropdown"; +import { useMemo, useState } from "react"; + +import { Button, buttonVariants } from "~/components/ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "~/components/ui/form"; +import { Input } from "~/components/ui/input"; +import { Textarea } from "~/components/ui/textarea"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "~/components/ui/collapsible"; + +import { cn } from "~/lib/utils"; +import { + ChevronDownIcon, + ChevronUpIcon, + HeightIcon, + PlusIcon, + ReloadIcon, + TrashIcon, +} from "@radix-ui/react-icons"; +import { SingleLessonPlanComponent } from "./LessonPlan"; +import { Checkbox } from "./ui/checkbox"; + +type CreateSchemaType = z.infer; + +const SaveButton = ({ + form, + onSave, + isSaving, +}: { + form: UseFormReturn; + onSave: () => void; + isSaving: boolean; +}) => { + const { toast } = useToast(); + + if (!form.formState.isValid) { + return ( + + ); + } + + return ( + + ); +}; + +const defaultEmptySection: CreateSchemaType["sections"][0] = { + title: "", + items: [], +}; + +const editFormDefaults: Partial = { + useDuration: true, + sections: [ + { + ...defaultEmptySection, + }, + ], +}; + +const SectionItems = ({ + sectionIndex, + form, + resources, + resourcesLoading, + durationEnabled, +}: { + sectionIndex: number; + form: UseFormReturn; + resources?: RouterOutputs["resource"]["getAllOnlyIdAndTitle"]; + resourcesLoading: boolean; + durationEnabled: boolean; +}) => { + const { + control, + formState: { errors }, + register, + } = form; + + const { + fields: items, + remove, + append, + update, + } = useFieldArray({ + control, + name: `sections.${sectionIndex}.items`, + }); + + const getErrorMessage = useMemo(() => { + return ( + sectionIndex: number, + itemIndex: number, + field: keyof CreateSchemaType["sections"][0]["items"][0], + ) => { + const errorMessage = + errors.sections?.[sectionIndex]?.items?.[itemIndex]?.[field]?.message; + + if (!errorMessage) { + return null; + } + + return {errorMessage}; + }; + }, [errors]); + + return ( + <> + {items.map((item, itemIndex) => ( + <> +
+
+ + +
+
+ Item {itemIndex + 1} +
+ {durationEnabled && ( + ( + + +
+
+ + Duration (mins): + +
+ +
+
+ {getErrorMessage(sectionIndex, itemIndex, "duration")} +
+ )} + /> + )} + { + if (field.value?.value === null) { + // This means the user selected "No Resource" + return <>; + } + + return ( + + + "Loading resources..."} + options={resources?.map(({ id, title }) => ({ + label: title, + value: id, + }))} + isClearable + /> + + {getErrorMessage(sectionIndex, itemIndex, "resource")} + + ); + }} + /> +
+
+ ( + + +