diff --git a/.vscode/settings.json b/.vscode/settings.json index dad68f6b..4022efe1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,6 @@ { "editor.codeActionsOnSave": { - "source.fixAll": true + "source.fixAll": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.formatOnSave": true, diff --git a/apps/academy/next.config.js b/apps/academy/next.config.js deleted file mode 100644 index 1b533a84..00000000 --- a/apps/academy/next.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = require("next-config/nextpwa.config")({ - // basePath: "/academy", -}); diff --git a/apps/academy/next.config.mjs b/apps/academy/next.config.mjs new file mode 100644 index 00000000..12f07085 --- /dev/null +++ b/apps/academy/next.config.mjs @@ -0,0 +1,64 @@ +import withPlugins from "next-compose-plugins"; +import withBundleAnalyzer from "@next/bundle-analyzer"; +import { PrismaPlugin } from "@prisma/nextjs-monorepo-workaround-plugin"; +import withPWA from "next-pwa"; +import remarkFrontmatter from "remark-frontmatter"; +import nextMDX from "@next/mdx"; + +const withMDX = nextMDX({ + extension: /\.mdx?$/, + options: { + remarkPlugins: [remarkFrontmatter], + rehypePlugins: [], + // If you use `MDXProvider`, uncomment the following line. + providerImportSource: "@mdx-js/react", + }, +}); + +/** @type {import('next').NextConfig} */ +const config = { + // basePath, + pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"], + reactStrictMode: true, + transpilePackages: ["ui", "utils", "database"], + webpack: (config, { isServer }) => { + config.resolve.fallback = { fs: false, net: false, tls: false }; + if (isServer) { + config.plugins = [...config.plugins, new PrismaPlugin()]; + } + return config; + }, + async headers() { + return [ + { + // matching all API routes + source: "/api/:path*", + headers: [ + { key: "Access-Control-Allow-Credentials", value: "true" }, + { key: "Access-Control-Allow-Origin", value: "*" }, + { + key: "Access-Control-Allow-Methods", + value: "GET,OPTIONS,PATCH,DELETE,POST,PUT", + }, + { + key: "Access-Control-Allow-Headers", + value: + "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version", + }, + ], + }, + ]; + }, +}; + +export default withPlugins( + [ + [withBundleAnalyzer({ enabled: !!process.env.ANALYZE })], + withPWA({ + dest: "public", + disable: process.env.NODE_ENV === "development", + }), + withMDX(), + ], + config, +); diff --git a/apps/academy/package.json b/apps/academy/package.json index e72f42f1..6c5a786b 100644 --- a/apps/academy/package.json +++ b/apps/academy/package.json @@ -42,10 +42,21 @@ "zod": "^3.22.2" }, "devDependencies": { + "@mdx-js/loader": "^3.0.0", + "@mdx-js/react": "^3.0.0", + "@next/bundle-analyzer": "13.4.12", + "@next/mdx": "^14.0.4", + "@prisma/nextjs-monorepo-workaround-plugin": "^5.3.1", + "@types/mdx": "^2.0.10", + "@types/react-syntax-highlighter": "^15.5.11", "jest-config": "workspace:*", "lighthouse-config": "workspace:*", + "next-compose-plugins": "^2.2.1", "next-config": "workspace:*", + "next-pwa": "^5.6.0", "playwright-config": "workspace:*", + "react-syntax-highlighter": "^15.5.0", + "remark-frontmatter": "^5.0.0", "storybook-config": "workspace:*", "tailwindcss-config": "workspace:*", "typescript-config": "workspace:*" diff --git a/apps/academy/src/components/AboutCourse.tsx b/apps/academy/src/components/AboutCourse.tsx index 169ef694..01208d15 100644 --- a/apps/academy/src/components/AboutCourse.tsx +++ b/apps/academy/src/components/AboutCourse.tsx @@ -1,13 +1,12 @@ -export default function AboutCourse() { +interface AboutCourseProps { + lessonDescription: string; +} + +export default function AboutCourse({ lessonDescription }: AboutCourseProps) { return (

About This Course

-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vehicula laoreet leo, - vehicula rhoncus lectus consectetur vel. Morbi sit amet fringilla erat, nec ultrices nulla. - Pellentesque habim volutpat, malesuada euismod turpis facilisis. Nunc non imperdiet eros. - Pellentesque lobortis justo a ligula efficitur congue. -

+

{lessonDescription}

); } diff --git a/apps/academy/src/components/CopyToClipboard.tsx b/apps/academy/src/components/CopyToClipboard.tsx new file mode 100644 index 00000000..d8bce51b --- /dev/null +++ b/apps/academy/src/components/CopyToClipboard.tsx @@ -0,0 +1,15 @@ +import { Button,Icons } from "ui"; + +interface Props { + children: string; +} + +export const CopyToClipboard = ({ children }: Props) => { + return ( +
+ +
+ ); +}; diff --git a/apps/academy/src/components/Header.tsx b/apps/academy/src/components/Header.tsx index fd9a9c61..b0ff86a6 100644 --- a/apps/academy/src/components/Header.tsx +++ b/apps/academy/src/components/Header.tsx @@ -14,7 +14,7 @@ const sampleMenus: NavItem[] = [ }, { name: "Tracks", - href: "/track/1", // hardcoded trackID for now. For the sake of using the dynamic route - 23 nov 2023 + href: "/intro-to-ethereum/1", // hardcoded trackID for now. For the sake of using the dynamic route - 23 nov 2023 icon: "vector", }, { diff --git a/apps/academy/src/components/LessonLayout.tsx b/apps/academy/src/components/LessonLayout.tsx new file mode 100644 index 00000000..3d55ce95 --- /dev/null +++ b/apps/academy/src/components/LessonLayout.tsx @@ -0,0 +1,34 @@ +import AboutCourse from "@/components/AboutCourse"; +import CreatedBy from "@/components/CreatedBy"; + +interface LessonLayoutProps { + children: React.ReactNode; + lessonTitle: string; + lessonDescription: string; +} + +export default function LessonLayout({ + children, + lessonTitle, + lessonDescription, +}: LessonLayoutProps) { + return ( +
+
+

{lessonTitle}

+
+
+
+
+ +
+
+ +
+
+
+ {children} +
+
+ ); +} diff --git a/apps/academy/src/components/Navbar.tsx b/apps/academy/src/components/Navbar.tsx deleted file mode 100644 index 3036ee2a..00000000 --- a/apps/academy/src/components/Navbar.tsx +++ /dev/null @@ -1,44 +0,0 @@ -// Component not used anymore - code implemented on Header Component 23 nov 2023 - -import Image from "next/image"; -import Link from "next/link"; -import { Avatar, AvatarFallback, AvatarImage } from "ui/components/ui/avatar"; - -export default function Navbar() { - return ( - - ); -} diff --git a/apps/academy/src/components/mdx/Callout.tsx b/apps/academy/src/components/mdx/Callout.tsx new file mode 100644 index 00000000..e383e0ff --- /dev/null +++ b/apps/academy/src/components/mdx/Callout.tsx @@ -0,0 +1,25 @@ +import React from "react"; + +export interface CalloutProps { + [x: string]: any; + // size?: string; + // variant?: string; + emoji: string; + children: React.ReactNode; +} + +const Callout = (props: CalloutProps): JSX.Element => { + const { /* size, variant, */ emoji, children, ...rest } = props; + + // const styles = useStyleConfig("Callout", { size, variant }); + return ( +
+
+
{emoji}
+
{children}
+
+
+ ); +}; + +export default Callout; diff --git a/apps/academy/src/components/mdx/Components.tsx b/apps/academy/src/components/mdx/Components.tsx new file mode 100644 index 00000000..f3a199e4 --- /dev/null +++ b/apps/academy/src/components/mdx/Components.tsx @@ -0,0 +1,43 @@ +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import dracula from "react-syntax-highlighter/dist/cjs/styles/prism/dracula"; + +import { CopyToClipboard } from "@/components/CopyToClipboard"; +import Callout from "@/components/mdx/Callout"; +import Question from "@/components/mdx/Question"; +// import SideDrawer from "@/components/mdx/SideDrawer"; +import Quiz from "@/components/mdx/Quiz"; + +const Components = { + code: (props: any) => { + const [, language] = + props.className !== undefined && props.className.length > 0 + ? props.className.match(/language-(\w+)/) + : []; + + if (language !== undefined) { + return ( +
+ + +
+ ); + } + + // return ; + return
; + }, + h1: (props: any) =>

, + h2: (props: any) =>

, + h3: (props: any) =>

, + h4: (props: any) =>

, + p: (props: any) =>

, + a: (props: any) => , + ul: (props: any) =>

- +
diff --git a/apps/academy/src/server/api/root.ts b/apps/academy/src/server/api/root.ts index 0a794406..0a599cdd 100644 --- a/apps/academy/src/server/api/root.ts +++ b/apps/academy/src/server/api/root.ts @@ -1,13 +1,15 @@ -import { exampleRouter } from "@/server/api/routers/example"; +import { completedQuizzesRouter } from "@/server/api/routers/completedquizzes"; +import { lessonsRouter } from "@/server/api/routers/lessons"; import { createTRPCRouter } from "@/server/api/trpc"; - +export * from "database"; /** * This is the primary router for your server. * * All routers added in /api/routers should be manually added here. */ export const appRouter = createTRPCRouter({ - example: exampleRouter, + lessons: lessonsRouter, + completedQuizzes: completedQuizzesRouter, }); // export type definition of API diff --git a/apps/academy/src/server/api/routers/completedquizzes.ts b/apps/academy/src/server/api/routers/completedquizzes.ts new file mode 100644 index 00000000..9ca8bad6 --- /dev/null +++ b/apps/academy/src/server/api/routers/completedquizzes.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + +// Imports +// ======================================================== +import { z } from "zod"; + +import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc"; + +// Router +// ======================================================== +export const completedQuizzesRouter = createTRPCRouter({ + /** + * Add completed Quizz belonging to the session user + */ + add: protectedProcedure + .input(z.object({ lesson: z.string() })) + .mutation(async ({ input, ctx }) => { + return await ctx.prisma.completedQuizzes.create({ + data: { + lesson: input.lesson, + userId: ctx.session.user.id, + completed: true, + }, + }); + }), + + /** + * All completed quizzes belonging to the session user + */ + all: protectedProcedure.query(async ({ ctx }) => { + const completedQuizzes = await ctx.prisma.completedQuizzes.findMany({ + where: { + userId: ctx.session.user.id, + }, + }); + return completedQuizzes; + }), +}); diff --git a/apps/academy/src/server/api/routers/lessons.ts b/apps/academy/src/server/api/routers/lessons.ts new file mode 100644 index 00000000..5a530e03 --- /dev/null +++ b/apps/academy/src/server/api/routers/lessons.ts @@ -0,0 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-return */ + +// Imports +// ======================================================== +import { createTRPCRouter, publicProcedure } from "@/server/api/trpc"; + +// Router +// ======================================================== +export const lessonsRouter = createTRPCRouter({ + getAll: publicProcedure.query(async ({ ctx }) => { + const lessons = await ctx.prisma.lessons.findMany(); + return lessons; + }), +}); diff --git a/apps/academy/src/utils/QuizHelpers.ts b/apps/academy/src/utils/QuizHelpers.ts new file mode 100644 index 00000000..99dc4626 --- /dev/null +++ b/apps/academy/src/utils/QuizHelpers.ts @@ -0,0 +1,29 @@ +interface Question { + question: string; + options: [ + { + answer: string; + correct?: boolean; + }, + ]; +} + +export function getCorrectAnswersIndexes(question: Question): number[] { + return question.options + .filter((option) => option.correct) + .map((correctOption) => question.options.indexOf(correctOption)); +} + +export function haveSameElements(arr1: number[], arr2: number[]): boolean { + if (arr1.length !== arr2.length) { + return false; + } + const arr1Sorted = arr1.sort(); + const arr2Sorted = arr2.sort(); + for (let i = 0; i < arr1Sorted.length; i++) { + if (arr1Sorted[i] !== arr2Sorted[i]) { + return false; + } + } + return true; +} diff --git a/packages/database/index.ts b/packages/database/index.ts index efe3ee8e..93881565 100644 --- a/packages/database/index.ts +++ b/packages/database/index.ts @@ -1,5 +1,7 @@ import { PrismaClient } from "./client"; +// import { PrismaClient } from "@prisma/client"; + // export * from "@prisma/client"; export * from "./client"; // testing because how prisma works in a monorepo changed many things diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index f3230613..49a04630 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -52,6 +52,7 @@ model CompletedQuizzes { model Lessons { id String @id @default(cuid()) - lessonNumber Int @unique @default(autoincrement()) + projectLessonNumber Int? @unique + fundamentalLessonName String? quizFileName String } diff --git a/packages/database/prisma/seed.ts b/packages/database/prisma/seed.ts index 43cc4931..882b9a18 100644 --- a/packages/database/prisma/seed.ts +++ b/packages/database/prisma/seed.ts @@ -1,5 +1,6 @@ import { PrismaClient } from "../client"; // import { hash } from "bcryptjs"; +// import { PrismaClient } from "@prisma/client"; const prisma = new PrismaClient(); diff --git a/packages/next-config/nextpwamdx.config.mjs b/packages/next-config/nextpwamdx.config.mjs new file mode 100644 index 00000000..4ac7e06a --- /dev/null +++ b/packages/next-config/nextpwamdx.config.mjs @@ -0,0 +1,66 @@ +import withPlugins from "next-compose-plugins"; +import withBundleAnalyzer from "@next/bundle-analyzer"; +import { PrismaPlugin } from "@prisma/nextjs-monorepo-workaround-plugin"; +import withPWA from "next-pwa"; +import remarkFrontmatter from "remark-frontmatter"; +import nextMDX from "@next/mdx"; + +const withMDX = nextMDX({ + extension: /\.mdx?$/, + options: { + remarkPlugins: [remarkFrontmatter], + rehypePlugins: [], + // If you use `MDXProvider`, uncomment the following line. + providerImportSource: "@mdx-js/react", + }, +}); + +module.exports = ({ basePath }) => { + /** @type {import('next').NextConfig} */ + const config = { + basePath, + pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"], + reactStrictMode: true, + transpilePackages: ["ui", "utils", "database"], + webpack: (config, { isServer }) => { + config.resolve.fallback = { fs: false, net: false, tls: false }; + if (isServer) { + config.plugins = [...config.plugins, new PrismaPlugin()]; + } + return config; + }, + async headers() { + return [ + { + // matching all API routes + source: "/api/:path*", + headers: [ + { key: "Access-Control-Allow-Credentials", value: "true" }, + { key: "Access-Control-Allow-Origin", value: "*" }, + { + key: "Access-Control-Allow-Methods", + value: "GET,OPTIONS,PATCH,DELETE,POST,PUT", + }, + { + key: "Access-Control-Allow-Headers", + value: + "X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version", + }, + ], + }, + ]; + }, + }; + + return withPlugins( + [ + [withBundleAnalyzer({ enabled: !!process.env.ANALYZE })], + withPWA({ + dest: "public", + disable: process.env.NODE_ENV === "development", + }), + withMDX(), + ], + config, + ); +}; diff --git a/packages/next-config/package.json b/packages/next-config/package.json index baf17195..c386b8c0 100644 --- a/packages/next-config/package.json +++ b/packages/next-config/package.json @@ -3,9 +3,13 @@ "name": "next-config", "version": "0.0.0", "devDependencies": { + "@mdx-js/loader": "^3.0.0", + "@mdx-js/react": "^3.0.0", "@next/bundle-analyzer": "13.4.12", + "@next/mdx": "^14.0.4", "@prisma/nextjs-monorepo-workaround-plugin": "^5.3.1", "next-compose-plugins": "^2.2.1", - "next-pwa": "^5.6.0" + "next-pwa": "^5.6.0", + "remark-frontmatter": "^5.0.0" } } diff --git a/packages/ui/package.json b/packages/ui/package.json index 04efe2f0..d5bec217 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -40,16 +40,19 @@ }, "devDependencies": { "@radix-ui/react-dialog": "^1.0.4", + "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-navigation-menu": "^1.1.3", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", "class-variance-authority": "^0.7.0", "jest-config": "workspace:*", "lucide-react": "^0.274.0", "storybook-config": "workspace:*", "tailwind-merge": "^1.14.0", "tailwindcss-config": "workspace:*", - "typescript-config": "workspace:*" + "typescript-config": "workspace:*", + "vaul": "^0.8.0" }, "dependencies": { "@radix-ui/react-avatar": "^1.0.4", diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx index dbf371f5..f99d7449 100644 --- a/packages/ui/src/components/Button/Button.tsx +++ b/packages/ui/src/components/Button/Button.tsx @@ -1,3 +1,4 @@ +import { ReloadIcon } from "@radix-ui/react-icons"; import clsx from "clsx"; import { type ButtonHTMLAttributes, forwardRef, type ForwardRefRenderFunction } from "react"; @@ -6,11 +7,19 @@ import type { Variant } from "./types"; export interface ButtonProps extends ButtonHTMLAttributes { variant?: Variant; + isLoading?: boolean; } const ButtonComponent: ForwardRefRenderFunction = (props, ref) => { - const { className, variant = "default", ...rest } = props; + const { className, variant = "default", isLoading = false, ...rest } = props; const finalClassName = clsx(commonStyles, variantStyles[variant], className); + if (isLoading) + return ( + + ); return