Skip to content
This repository has been archived by the owner on Nov 20, 2024. It is now read-only.

Dev firgrep #30

Merged
merged 6 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
567 changes: 465 additions & 102 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 12 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
"lint": "next lint"
},
"dependencies": {
"@google-cloud/storage": "^7.0.1",
"@google-cloud/storage": "^7.1.0",
"@mdx-js/mdx": "^2.3.0",
"@mdxeditor/editor": "^0.15.1",
"@mdxeditor/editor": "^1.0.0",
"@next-auth/prisma-adapter": "^1.0.7",
"@prisma/client": "^5.1.1",
"@prisma/client": "^5.3.1",
"@tailwindcss/typography": "^0.5.9",
"@tanstack/react-query": "^4.33.0",
"@trpc/client": "^10.37.1",
Expand All @@ -28,24 +28,28 @@
"clsx": "^2.0.0",
"encoding": "^0.1.13",
"eslint": "8.47.0",
"eslint-config-next": "13.4.19",
"next": "13.4.19",
"eslint-config-next": "^13.5.2",
"next": "^13.5.2",
"next-auth": "^4.23.1",
"postcss": "8.4.28",
"react": "18.2.0",
"react-dom": "18.2.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.45.4",
"react-hot-toast": "^2.4.1",
"react-textarea-autosize": "^8.5.3",
"remark-gfm": "^3.0.1",
"request": "^2.88.2",
"superjson": "^1.13.1",
"tailwindcss": "3.3.3",
"typescript": "5.1.6",
"zod": "^3.22.2"
},
"overrides": {
"tough-cookie": "4.1.3"
},
"devDependencies": {
"daisyui": "^3.6.1",
"prisma": "^5.1.1",
"prisma": "^5.3.1",
"ts-node": "^10.9.1"
}
}
107 changes: 79 additions & 28 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ datasource db {
url = env("DATABASE_URL")
}

// User Access and Permissions (necessary for NextAuth)
// === SECTION 1 ==============================================
// * User Access and Permissions (necessary for NextAuth)
// ============================================================
model Account {
id String @id @default(cuid())
userId String
Expand Down Expand Up @@ -49,8 +51,8 @@ model User {
accounts Account[]
sessions Session[]

stripeCustomerId String? @unique
stripePurchasedProducts String[]
stripeCustomerId String? @unique
stripePurchasedProducts StripeProduct[]

lessonsCompleted UserLessonProgress[]
}
Expand All @@ -68,13 +70,15 @@ model VerificationToken {
@@unique([identifier, token])
}

// Course, Lessons and Content related data
// === SECTION 2 ==============================================
// * Course, Lessons and Content related data
// ============================================================
model Course {
id String @id @default(cuid())
name String
description String
slug String @unique
stripeProductId String? @unique
stripeProductId StripeProduct?
imageUrl String?
parts Part[]
lessons Lesson[]
Expand All @@ -83,20 +87,27 @@ model Course {
published Boolean @default(false)
}

enum MdxCategory {
CONTENT
TRANSCRIPT
DETAILS
}

model CourseDetails {
id String @id @default(cuid())
course Course @relation(fields: [courseId], references: [id])
courseId String @unique
content Bytes
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
courseId String @unique
mdxCategory MdxCategory @default(DETAILS)
mdx Bytes
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Part {
id String @id @default(cuid())
name String
slug String
course Course @relation(fields: [courseId], references: [id])
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
courseId String
lessons Lesson[]
}
Expand All @@ -108,7 +119,7 @@ model Lesson {
slug String
part Part? @relation(fields: [partId], references: [id])
partId String?
course Course @relation(fields: [courseId], references: [id])
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
courseId String
content LessonContent?
transcript LessonTranscript?
Expand All @@ -117,38 +128,78 @@ model Lesson {
}

model LessonContent {
id String @id @default(cuid())
lesson Lesson @relation(fields: [lessonId], references: [id])
lessonId String @unique
content Bytes
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
lessonId String @unique
mdxCategory MdxCategory @default(CONTENT)
mdx Bytes
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model LessonTranscript {
id String @id @default(cuid())
lesson Lesson @relation(fields: [lessonId], references: [id])
lessonId String @unique
transcript Bytes
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
lessonId String @unique
mdxCategory MdxCategory @default(TRANSCRIPT)
mdx Bytes
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model Video {
id String @id @default(cuid())
lesson Lesson? @relation(fields: [lessonId], references: [id])
lesson Lesson? @relation(fields: [lessonId], references: [id], onDelete: Cascade)
lessonId String? @unique
fileName String
duration Float?
posterUrl String?
}

model UserLessonProgress {
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId String
lesson Lesson @relation(fields: [lessonId], references: [id])
lesson Lesson @relation(fields: [lessonId], references: [id], onDelete: Cascade)
lessonId String
completedAt DateTime @default(now())

@@id([userId, lessonId])
}

// === SECTION 3 ==============================================
// * Stripe related data
// ============================================================
model StripeProduct {
id String @id @default(cuid())
productId String @unique
interlinkedModel Course? @relation(fields: [interlinkedModelId], references: [id], onDelete: Cascade)
interlinkedModelId String? @unique
users User[]
prices StripePrice[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model StripePrice {
id String @id @default(cuid())
priceId String @unique
amount Float
currency String
product StripeProduct @relation(fields: [productId], references: [id], onDelete: Cascade)
productId String @unique
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

model StripeEvent {
id String @id @unique
api_version String?
data Json
request Json?
type String
object String
account String?
created DateTime
livemode Boolean
pending_webhooks Int
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Editor from "@/components/Editor";
import { dbGetCourseAndDetailsAndLessonsById, dbGetMdxByModelId } from "@/server/controllers/coursesController";

/**
* Fetches data for CourseDetails and renders the MDX Editor to the UI.
*/
export default async function AdminLessonMaterialEdit ({
params
}: {
params: { courseId: string, courseDetailsId: string }
}) {
const courseId = params.courseId;
const courseDetailsId = params.courseDetailsId;
if (typeof courseDetailsId !== "string") { throw new Error("missing lessonContent id") };

const editorMaterial = await dbGetMdxByModelId(courseDetailsId);
const course = await dbGetCourseAndDetailsAndLessonsById(courseId);

if (!editorMaterial) {
throw new Error("CourseDetails not found");
}
if (!course) {
throw new Error("Course not found");
}

return(
<Editor initialMaterial={editorMaterial} title={course.name}/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { dbGetCourseAndDetailsAndLessonsById, dbUpsertCourseDetailsById } from "@/server/controllers/coursesController";
import { redirect } from "next/navigation";

/**
* Intermediate route that creates a new CourseDetails entry unless it exists and pushes the user to that route.
* @description Should never display any UI
*/
export default async function AdminCourseDetailsNew ({ params }: { params: { courseId: string} }) {

const courseId = params.courseId;
if (typeof courseId !== "string") { throw new Error("missing course or lesson id") };

const course = await dbGetCourseAndDetailsAndLessonsById(courseId);

if (course && course.details && course.details.id) {
// If CourseDetails already exists, push to that.
redirect(`/admin/courses/${courseId}/course-details/${course.details.id}`);
}
const newCourseDetails = {
courseId: courseId,
content: String(`Hello **new course details for ${course?.name}!**`),
}

const result = await dbUpsertCourseDetailsById(newCourseDetails);

if (result && result.id) {
// If new LessonContent entry was created successfully, push user to route.
redirect(`/admin/courses/${courseId}/course-details/${result.id}`);
}

return(
<p>You should never see this message. An error occured somewhere.</p>
)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Editor from "@/components/Editor";
import { dbGetLessonAndRelationsById, dbGetLessonContentOrLessonTranscriptById } from "@/server/controllers/coursesController";
import { dbGetLessonAndRelationsById, dbGetMdxByModelId } from "@/server/controllers/coursesController";

/**
* Common route for models LessonContent and LessonTranscript that fetches respective data and renders the MDX Editor to the UI.
Expand All @@ -14,7 +14,7 @@ export default async function AdminLessonMaterialEdit ({
const lessonMaterialId = params.lessonMaterialId;
if (typeof lessonMaterialId !== "string") { throw new Error("missing lessonContent id") };

const lessonMaterial = await dbGetLessonContentOrLessonTranscriptById(lessonMaterialId);
const lessonMaterial = await dbGetMdxByModelId(lessonMaterialId);
const lesson = await dbGetLessonAndRelationsById(lessonId);

if (!lessonMaterial) {
Expand All @@ -25,6 +25,6 @@ export default async function AdminLessonMaterialEdit ({
}

return(
<Editor initialLessonMaterial={lessonMaterial} lessonName={lesson.name}/>
<Editor initialMaterial={lessonMaterial} title={lesson.name}/>
)
}
30 changes: 25 additions & 5 deletions src/app/(admin)/admin/courses/[courseId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import Link from 'next/link'
import { dbGetCourseAndLessonsById } from '@/server/controllers/coursesController';
import { dbGetCourseAndDetailsAndLessonsById } from '@/server/controllers/coursesController';
import { redirect } from "next/navigation";
import Heading from '@/components/Heading';
import AdminCourseFormContainer from '@/components/AdminCourseFormContainer'
import CourseMaterialCard from '@/components/CourseMaterialCard';
import toast from 'react-hot-toast';


export default async function AdminCourseEdit ({ params }: { params: { courseId: string }}) {
const id = params.courseId;
if (typeof id !== "string") { throw new Error('missing course id') };
const courseId = params.courseId;
if (typeof courseId !== "string") { throw new Error('missing course id') };

const course = await dbGetCourseAndLessonsById(id);
const course = await dbGetCourseAndDetailsAndLessonsById(courseId);

if (!course) {
toast.error("Oops! No course was found here!")
console.log("No course found");
redirect("/");
}
Expand All @@ -21,7 +23,25 @@ export default async function AdminCourseEdit ({ params }: { params: { courseId:
<div className='grid md:grid-cols-2'>
<div>
<Heading as='h2'>{course.name}</Heading>
<AdminCourseFormContainer initialCourse={course} id={id}/>
<AdminCourseFormContainer initialCourse={course} id={courseId}/>
<div className="mt-6">
<Heading as='h4'>Course Details</Heading>
{(
course.details
) ? (
<CourseMaterialCard
href={`/admin/courses/${courseId}/course-details/${course.details.id}`}
heading="General details of the course"
/>
) : (
<div>
<Heading as='h2'>Currently no details.</Heading>
<Link href={`/admin/courses/${courseId}/course-details/new`}>
<button className="btn btn-primary">Add details</button>
</Link>
</div>
)}
</div>
</div>

<div>
Expand Down
3 changes: 2 additions & 1 deletion src/app/(admin)/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { getServerAuthSession } from "@/server/auth";
import { redirect } from "next/navigation";


export const dynamic = 'force-dynamic'; // Nextjs flag that disables all caching of fetch requests and always invalidates routes on /admin/*
export const dynamic = 'force-dynamic'; // Nextjs flags that disables all caching of fetch requests and always invalidates routes on /admin/*
export const revalidate = 0;

/**
* AdminLayout controls the access and UI for /admin/**
Expand Down
4 changes: 0 additions & 4 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { apiServerside } from "../lib/trpc/trpcServerside"
import AuthShowcase from "../components/test/AuthShowcase";
import TodoList from "../components/test/TodoList";
import ToastTest from "@/components/test/ToastTest";

export default async function Home() {
const todos = await apiServerside.fiction.getTodos();

return (
<main className="h-screen flex flex-col justify-front items-center gap-4 bg-slate-200">
Expand All @@ -14,7 +11,6 @@ export default async function Home() {
//TODO To be removed
*/}
<AuthShowcase />
<TodoList initialTodos={todos}/>
<ToastTest />
</main>
)
Expand Down
Loading