From b2d9d2bbe67c1aaac2d1d98068f92eadf2fdb472 Mon Sep 17 00:00:00 2001 From: austinkelsay <austinkelsay@yahoo.com> Date: Sat, 24 Aug 2024 15:55:59 -0500 Subject: [PATCH] Improvements to courseform ui, fix draftcourse delete --- prisma/schema.prisma | 2 + src/components/content/SelectedContentItem.js | 16 +++- .../content/courses/DraftCourseDetails.js | 2 + .../content/courses/DraftCourseLesson.js | 9 ++ src/components/forms/course/LessonSelector.js | 95 +++++++++++++------ src/db/models/courseDraftModels.js | 12 ++- src/utils/time.js | 13 ++- 7 files changed, 110 insertions(+), 39 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 0ead04f..472a96a 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -80,6 +80,7 @@ model Course { updatedAt DateTime @updatedAt } +// Additional resources model Resource { id String @id // Client generates UUID userId String @@ -93,6 +94,7 @@ model Resource { updatedAt DateTime @updatedAt } +// Additional resources model Draft { id String @id @default(uuid()) userId String diff --git a/src/components/content/SelectedContentItem.js b/src/components/content/SelectedContentItem.js index 3f737da..72888ec 100644 --- a/src/components/content/SelectedContentItem.js +++ b/src/components/content/SelectedContentItem.js @@ -2,12 +2,22 @@ import React from "react"; import Image from "next/image"; import { useImageProxy } from "@/hooks/useImageProxy"; import { formatUnixTimestamp } from "@/utils/time"; +import { Button } from 'primereact/button'; -const SelectedContentItem = ({ content }) => { +const SelectedContentItem = ({ content, onRemove }) => { const { returnImageProxy } = useImageProxy(); return ( - <div className="w-full border-2 rounded-lg border-gray-700 p-2 rounded-tr-none rounded-br-none"> + <div className="w-full border-2 rounded-lg border-gray-700 p-2 rounded-tr-none rounded-br-none relative"> + <Button + icon="pi pi-times" + className="absolute top-2 right-2 py-1 px-2 w-auto h-auto" + severity="danger" + size="small" + rounded + onClick={onRemove} + aria-label="Remove" + /> <div className="flex flex-row gap-4"> <Image alt="content thumbnail" @@ -28,4 +38,4 @@ const SelectedContentItem = ({ content }) => { ); }; -export default SelectedContentItem; +export default SelectedContentItem; \ No newline at end of file diff --git a/src/components/content/courses/DraftCourseDetails.js b/src/components/content/courses/DraftCourseDetails.js index 684f51a..2573edc 100644 --- a/src/components/content/courses/DraftCourseDetails.js +++ b/src/components/content/courses/DraftCourseDetails.js @@ -13,6 +13,7 @@ import { useNDKContext } from "@/context/NDKContext"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import { findKind0Fields } from '@/utils/nostr'; import { useToast } from '@/hooks/useToast'; +import { formatDateTime } from '@/utils/time'; import 'primeicons/primeicons.css'; const MDDisplay = dynamic( @@ -353,6 +354,7 @@ export default function DraftCourseDetails({ processedEvent, draftId, lessons }) </a> </p> </div> + <p className="pt-8 text-sm text-gray-400">{processedEvent?.createdAt && formatDateTime(processedEvent?.createdAt)}</p> </div> <div className='flex flex-col max-tab:mt-12 max-mob:mt-12'> {processedEvent && ( diff --git a/src/components/content/courses/DraftCourseLesson.js b/src/components/content/courses/DraftCourseLesson.js index 86c1dd5..92fa509 100644 --- a/src/components/content/courses/DraftCourseLesson.js +++ b/src/components/content/courses/DraftCourseLesson.js @@ -3,6 +3,7 @@ import { Tag } from "primereact/tag"; import { Message } from "primereact/message"; import Image from "next/image"; import { useImageProxy } from "@/hooks/useImageProxy"; +import { formatDateTime, formatUnixTimestamp } from "@/utils/time"; import dynamic from "next/dynamic"; const MDDisplay = dynamic( @@ -17,6 +18,7 @@ const DraftCourseLesson = ({ lesson, course }) => { const [isPublished, setIsPublished] = useState(false); useEffect(() => { if (lesson?.kind) { + console.log(lesson); setIsPublished(true); } else { setIsPublished(false); @@ -52,6 +54,13 @@ const DraftCourseLesson = ({ lesson, course }) => { </a> </p> </div> + { + lesson?.createdAt ? ( + <p className="pt-8 text-sm text-gray-400">{formatDateTime(lesson?.createdAt)}</p> + ) : ( + <p className="pt-8 text-sm text-gray-400">{formatUnixTimestamp(lesson?.published_at)}</p> + ) + } <div className='flex flex-row w-full mt-6 items-center'> {isPublished ? ( <Message severity="success" text="published" /> diff --git a/src/components/forms/course/LessonSelector.js b/src/components/forms/course/LessonSelector.js index d8eaa66..b53c80d 100644 --- a/src/components/forms/course/LessonSelector.js +++ b/src/components/forms/course/LessonSelector.js @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'; import { Dropdown } from 'primereact/dropdown'; import { Button } from 'primereact/button'; import { Dialog } from 'primereact/dialog'; +import { Accordion, AccordionTab } from 'primereact/accordion'; import ResourceForm from '../ResourceForm'; import WorkshopForm from '../WorkshopForm'; import ContentDropdownItem from '@/components/content/dropdowns/ContentDropdownItem'; @@ -82,18 +83,31 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent }) => { console.log("contentOptions", contentOptions); }, [contentOptions]); - const handleContentSelect = (selectedContent) => { - if (selectedContent && !lessons.some(lesson => lesson.id === selectedContent.id)) { - setLessons([...lessons, { ...selectedContent, index: lessons.length }]); + const handleContentSelect = (selectedContent, index) => { + if (selectedContent) { + const updatedLessons = [...lessons]; + updatedLessons[index] = { ...selectedContent, index }; + setLessons(updatedLessons); } }; + const handleRemoveContent = (index) => { + const updatedLessons = [...lessons]; + updatedLessons[index] = { index }; // Reset the lesson to an empty state + setLessons(updatedLessons); + }; + const removeLesson = (index) => { const updatedLessons = lessons.filter((_, i) => i !== index) .map((lesson, newIndex) => ({ ...lesson, index: newIndex })); setLessons(updatedLessons); }; + const addNewLesson = (e) => { + e.preventDefault(); // Prevent form submission + setLessons([...lessons, { index: lessons.length }]); + }; + const handleNewResourceSave = (newResource) => { setLessons([...lessons, { ...newResource, index: lessons.length }]); setShowResourceForm(false); @@ -104,35 +118,58 @@ const LessonSelector = ({ isPaidCourse, lessons, setLessons, allContent }) => { setShowWorkshopForm(false); }; + const AccordianHeader = ({lesson, index}) => { + return ( + <div className="flex justify-between items-center"> + <p>Lesson {index + 1}</p> + <Button icon="pi pi-times" className="p-button-danger" onClick={() => removeLesson(index)} /> + </div> + ); + }; + return ( <div className="mt-8"> <h3>Lessons</h3> - {lessons.map((lesson, index) => ( - <div key={lesson.id} className="flex mt-4"> - <SelectedContentItem content={{ ...lesson, index }} /> - <Button - icon="pi pi-times" - className="p-button-danger rounded-tl-none rounded-bl-none" - onClick={() => removeLesson(index)} - /> - </div> - ))} - <div className="p-inputgroup flex-1 mt-4"> - <Dropdown - options={contentOptions} - onChange={(e) => handleContentSelect(e.value)} - placeholder="Select Existing Lesson" - optionLabel="label" - optionGroupLabel="label" - optionGroupChildren="items" - itemTemplate={(option) => <ContentDropdownItem content={option.value} onSelect={handleContentSelect} />} - value={null} - /> - </div> - <div className="flex mt-4"> - <Button label="New Resource" onClick={() => setShowResourceForm(true)} className="mr-2" /> - <Button label="New Workshop" onClick={() => setShowWorkshopForm(true)} /> - </div> + <Accordion multiple> + {lessons.map((lesson, index) => ( + <AccordionTab key={index} header={<AccordianHeader lesson={lesson} index={index} />}> + <div className="p-inputgroup flex-1 mt-4"> + <Dropdown + options={contentOptions} + onChange={(e) => handleContentSelect(e.value, index)} + placeholder="Select Existing Lesson" + optionLabel="label" + optionGroupLabel="label" + optionGroupChildren="items" + itemTemplate={(option) => <ContentDropdownItem content={option.value} onSelect={(content) => handleContentSelect(content, index)} />} + value={lesson.id ? lesson : null} + /> + </div> + <div className="flex mt-4"> + {lesson.id ? null : ( + <> + <Button label="New Resource" onClick={() => setShowResourceForm(true)} className="mr-2" /> + <Button label="New Workshop" onClick={() => setShowWorkshopForm(true)} className="mr-2" /> + </> + )} + </div> + {lesson.id && ( + <div className="mt-4"> + <SelectedContentItem + content={lesson} + onRemove={() => handleRemoveContent(index)} + /> + </div> + )} + </AccordionTab> + ))} + </Accordion> + <Button + label="Add New Lesson" + onClick={addNewLesson} + className="mt-4" + type="button" // Explicitly set type to "button" + /> <Dialog visible={showResourceForm} onHide={() => setShowResourceForm(false)} header="Create New Resource"> <ResourceForm onSave={handleNewResourceSave} isPaid={isPaidCourse} /> diff --git a/src/db/models/courseDraftModels.js b/src/db/models/courseDraftModels.js index e14629a..871ecb6 100644 --- a/src/db/models/courseDraftModels.js +++ b/src/db/models/courseDraftModels.js @@ -91,7 +91,15 @@ export const updateCourseDraft = async (id, data) => { // Delete a CourseDraft by its ID export const deleteCourseDraft = async (id) => { - return await prisma.courseDraft.delete({ - where: { id }, + return await prisma.$transaction(async (prisma) => { + // First, delete all associated DraftLessons + await prisma.draftLesson.deleteMany({ + where: { courseDraftId: id }, + }); + + // Then, delete the CourseDraft + return await prisma.courseDraft.delete({ + where: { id }, + }); }); }; \ No newline at end of file diff --git a/src/utils/time.js b/src/utils/time.js index 0abe0c7..b3aad10 100644 --- a/src/utils/time.js +++ b/src/utils/time.js @@ -1,6 +1,12 @@ export const formatUnixTimestamp = (time) => { const date = new Date(time * 1000); // Convert to milliseconds - return date.toDateString(); + return date.toLocaleDateString("en-US", { + timeZone: "UTC", + weekday: "long", + year: "numeric", + month: "long", + day: "numeric", + }); } export const formatDateTime = (isoDate) => { @@ -12,10 +18,7 @@ export const formatDateTime = (isoDate) => { weekday: "long", // "long" for full name, "short" for abbreviated year: "numeric", month: "long", // "long" for full name, "short" for abbreviated - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", + day: "numeric" }); return formattedDate;