├── api ├── .prettierrc.json ├── .dockerignore ├── README.md ├── src │ ├── common │ │ ├── constants │ │ │ └── texts.ts │ │ ├── db.ts │ │ ├── text.ts │ │ ├── config.ts │ │ ├── http-exception.ts │ │ └── zod-schemas.ts │ ├── healthchecks │ │ └── index.ts │ ├── middleware │ │ ├── not-found.middleware.ts │ │ └── error.middleware.ts │ ├── courses │ │ ├── get-all-courses.handler.ts │ │ ├── modules │ │ │ ├── delete-module.handler.ts │ │ │ ├── module.router.ts │ │ │ ├── update-module.handler.ts │ │ │ ├── create-module.handler.ts │ │ │ └── update-module-order.ts │ │ ├── get-my-courses.handler.ts │ │ ├── archieve-course-handler.ts │ │ ├── update-live-link.handler.ts │ │ ├── update-course.handler.ts │ │ ├── create-course.handler.ts │ │ ├── courses.router.ts │ │ └── get-course.handler.ts │ ├── index.ts │ └── app.ts ├── prisma │ ├── migrations │ │ ├── migration_lock.toml │ │ └── 20221110085013_init │ │ │ └── migration.sql │ └── schema.prisma ├── Dockerfile ├── .eslintrc.js ├── docker-compose.yml ├── package.json ├── .prettierignore ├── .gitignore └── tsconfig.json ├── webapp ├── .prettierrc ├── .dockerignore ├── .env ├── src │ ├── types │ │ ├── declarations.d.ts │ │ ├── common.ts │ │ └── courses.ts │ ├── vite-env.d.ts │ ├── App.css │ ├── assets │ │ ├── banner.jpg │ │ ├── course.png │ │ ├── logo.png │ │ ├── user.png │ │ ├── dl-logo.png │ │ ├── welcome.jpg │ │ └── illustration-login.png │ ├── __snapshots__ │ │ └── App.test.tsx.snap │ ├── components │ │ ├── TopBarLoader.tsx │ │ ├── IF.tsx │ │ ├── vimeo-video.css │ │ ├── Text.tsx │ │ ├── Banner.tsx │ │ ├── ImageFilePreview.tsx │ │ ├── AdminSideBar.tsx │ │ ├── Resources.tsx │ │ ├── Assignments.tsx │ │ ├── Projects.tsx │ │ ├── NoCourses.tsx │ │ ├── VimeoVideo.tsx │ │ ├── Modules.tsx │ │ ├── AllCoursesSideBar.tsx │ │ ├── CourseCard.tsx │ │ ├── Modal.tsx │ │ ├── CourseInformationTabs.tsx │ │ ├── UpdateFiles.tsx │ │ ├── CourseInfo.tsx │ │ └── Module.tsx │ ├── lib │ │ ├── http-client.ts │ │ ├── constants.ts │ │ ├── errors.ts │ │ ├── test-utils │ │ │ └── index.ts │ │ ├── react-query.ts │ │ └── strings.ts │ ├── index.css │ ├── mocks │ │ ├── server.ts │ │ ├── browser.ts │ │ ├── handlers.ts │ │ └── mock-data │ │ │ └── courses.ts │ ├── setup.ts │ ├── App.test.tsx │ ├── hooks │ │ ├── useTopbarLoader.ts │ │ ├── useGetUserPermissions.ts │ │ ├── useGetCourse.ts │ │ ├── useGetCourses.ts │ │ ├── useCourseFilesUpdateMutation.tsx │ │ ├── useGetAllCourses.tsx │ │ ├── verifyEmailMutation.ts │ │ └── usePasswordResetMutation.ts │ ├── env.d.ts │ ├── pages │ │ ├── ManageCourses.tsx │ │ ├── admin.tsx │ │ ├── ManageCourse.tsx │ │ ├── dashboard.tsx │ │ ├── create-course.tsx │ │ └── course.tsx │ ├── App.tsx │ ├── main.tsx │ ├── layouts │ │ └── MainLayout.tsx │ └── routes.tsx ├── public │ ├── dl-logo.png │ ├── vite.svg │ └── mockServiceWorker.js ├── postcss.config.cjs ├── staticwebapp.config.json ├── tsconfig.node.json ├── .gitignore ├── .prettierignore ├── vite.config.ts ├── index.html ├── .eslintrc.cjs ├── tsconfig.json ├── tailwind.config.cjs ├── Dockerfile ├── README.md ├── nginx.conf └── package.json ├── README.md ├── deploy.sh ├── docker-compose.yml └── Jenkinsfile /api/.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /webapp/.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /api/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # Konamars LMS API -------------------------------------------------------------------------------- /webapp/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /webapp/.env: -------------------------------------------------------------------------------- 1 | VITE_API_URL=http://localhost:8080/api 2 | -------------------------------------------------------------------------------- /webapp/src/types/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module "topbar"; 2 | -------------------------------------------------------------------------------- /webapp/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /webapp/src/App.css: -------------------------------------------------------------------------------- 1 | .banner { 2 | background-image: url("./assets/banner.jpg"); 3 | } 4 | -------------------------------------------------------------------------------- /api/src/common/constants/texts.ts: -------------------------------------------------------------------------------- 1 | export const fromEmailAddress = "support@learn.konamars.com"; -------------------------------------------------------------------------------- /webapp/public/dl-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravi2krishna/lms/HEAD/webapp/public/dl-logo.png -------------------------------------------------------------------------------- /webapp/src/assets/banner.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravi2krishna/lms/HEAD/webapp/src/assets/banner.jpg -------------------------------------------------------------------------------- /webapp/src/assets/course.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravi2krishna/lms/HEAD/webapp/src/assets/course.png -------------------------------------------------------------------------------- /webapp/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravi2krishna/lms/HEAD/webapp/src/assets/logo.png -------------------------------------------------------------------------------- /webapp/src/assets/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravi2krishna/lms/HEAD/webapp/src/assets/user.png -------------------------------------------------------------------------------- /webapp/src/assets/dl-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravi2krishna/lms/HEAD/webapp/src/assets/dl-logo.png -------------------------------------------------------------------------------- /webapp/src/assets/welcome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravi2krishna/lms/HEAD/webapp/src/assets/welcome.jpg -------------------------------------------------------------------------------- /api/src/common/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | export const db = new PrismaClient(); 4 | -------------------------------------------------------------------------------- /webapp/src/__snapshots__/App.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`App > Renders correctly 1`] = `
`; 4 | -------------------------------------------------------------------------------- /webapp/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /webapp/src/assets/illustration-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ravi2krishna/lms/HEAD/webapp/src/assets/illustration-login.png -------------------------------------------------------------------------------- /webapp/src/components/TopBarLoader.tsx: -------------------------------------------------------------------------------- 1 | function TopBarLoader() { 2 | return null; 3 | } 4 | 5 | export default TopBarLoader; 6 | -------------------------------------------------------------------------------- /api/src/common/text.ts: -------------------------------------------------------------------------------- 1 | export function capitalize(text: string) { 2 | return text.charAt(0).toUpperCase() + text.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning Management System 2 | 3 | ## REACT JS - Presentation tier 4 | ## NODE JS - Application tier 5 | ## POSTGRES - Data tier 6 | -------------------------------------------------------------------------------- /webapp/staticwebapp.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationFallback": { 3 | "rewrite": "/index.html", 4 | "exclude": ["*.{jpg,gif,png}"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /api/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /webapp/src/lib/http-client.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const axiosInstance = axios.create({ 4 | baseURL: import.meta.env.VITE_API_URL, 5 | }); 6 | 7 | export default axiosInstance; 8 | -------------------------------------------------------------------------------- /webapp/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .btn { 6 | @apply rounded-none; 7 | } 8 | 9 | .bar { 10 | @apply bg-primary; 11 | } 12 | -------------------------------------------------------------------------------- /webapp/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { Tabs } from "../types/courses"; 2 | 3 | export const TabsList: Tabs[] = [ 4 | "Course Info", 5 | "Resources", 6 | "Assignments", 7 | "Projects", 8 | ]; 9 | -------------------------------------------------------------------------------- /webapp/src/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from "msw/node"; 2 | import { handlers } from "./handlers"; 3 | // This configures a Service Worker with the given request handlers. 4 | export const server = setupServer(...handlers); 5 | -------------------------------------------------------------------------------- /webapp/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /webapp/src/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | // src/mocks/browser.js 2 | import { setupWorker } from "msw"; 3 | import { handlers } from "./handlers"; 4 | 5 | // This configures a Service Worker with the given request handlers. 6 | export const worker = setupWorker(...handlers); 7 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | # deploy using docker compose 2 | 3 | cd ~/lms-public 4 | 5 | git pull 6 | 7 | if curl http://localhost:3000; then 8 | docker compose down 9 | fi 10 | 11 | docker compose up -d --build "api-server" "web-server" 12 | 13 | echo "Deployment done!" 14 | -------------------------------------------------------------------------------- /webapp/src/components/IF.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type IFProps = { 4 | condition: boolean; 5 | children: React.ReactNode; 6 | }; 7 | 8 | export default function IF(props: IFProps) { 9 | return props.condition ? <>{props.children} : null; 10 | } 11 | -------------------------------------------------------------------------------- /webapp/src/setup.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, afterAll, afterEach } from "vitest"; 2 | import { server } from "./mocks/server"; 3 | 4 | beforeAll(() => server.listen({ onUnhandledRequest: "error" })); 5 | afterAll(() => server.close()); 6 | afterEach(() => server.resetHandlers()); 7 | -------------------------------------------------------------------------------- /webapp/src/components/vimeo-video.css: -------------------------------------------------------------------------------- 1 | .vimeo-full-width { 2 | /* The top padding is 56.25% because of aspect ratio 16:9 */ 3 | padding: 56.25% 0 0 0; 4 | position: relative; 5 | } 6 | 7 | .vimeo-full-width iframe { 8 | position: absolute; 9 | top: 0; 10 | left: 0; 11 | width: 100%; 12 | height: 100%; 13 | } 14 | -------------------------------------------------------------------------------- /webapp/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { render } from "@testing-library/react"; 3 | import App from "./App"; 4 | 5 | describe("App", () => { 6 | it("Renders correctly", () => { 7 | const screen = render(); 8 | expect(screen.container).toMatchSnapshot(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /webapp/src/components/Text.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import React from "react"; 3 | 4 | function Text(props: { children: React.ReactNode; className?: string }) { 5 | return ( 6 |
7 | {props.children} 8 |
9 | ); 10 | } 11 | 12 | export default Text; 13 | -------------------------------------------------------------------------------- /api/src/common/config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv"; 2 | import path from "path"; 3 | 4 | dotenv.config({ 5 | path: path.join(__dirname, "..", "..", ".env"), 6 | }); 7 | 8 | export const getConfig = (key: string) => { 9 | if (!key) { 10 | throw new Error("Config key can't be empty."); 11 | } 12 | 13 | return process.env[key]; 14 | }; -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | # Docker file nodejs azure app services 2 | FROM node:latest 3 | # Create app directory 4 | WORKDIR /app 5 | # Install app dependencies 6 | COPY package*.json ./ 7 | RUN npm install 8 | # Build app 9 | COPY . . 10 | RUN npx prisma generate 11 | RUN npm run build 12 | # Expose port 13 | EXPOSE 8080 14 | # Start app 15 | CMD [ "npm", "start" ] 16 | -------------------------------------------------------------------------------- /webapp/src/hooks/useTopbarLoader.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import topbar from "topbar"; 3 | 4 | function useTopbarLoader(showLoader: boolean) { 5 | useEffect(() => { 6 | if (showLoader) { 7 | topbar.show(); 8 | } else { 9 | topbar.hide(); 10 | } 11 | }, [showLoader]); 12 | } 13 | 14 | export default useTopbarLoader; 15 | -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /webapp/src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { APIError } from "../types/common"; 3 | 4 | export const axiosAPIErrorHandler = (error: unknown) => { 5 | if (axios.isAxiosError(error)) { 6 | const err = error.response?.data as APIError; 7 | throw new Error(err.message); 8 | } 9 | throw new Error("Something went wrong, please try later."); 10 | }; 11 | -------------------------------------------------------------------------------- /webapp/.prettierignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /webapp/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export interface APIError { 2 | message: string; 3 | } 4 | 5 | export interface ProfileUpdateResponse { 6 | name: string; 7 | email: string; 8 | pictrue: string; 9 | email_verified: boolean; 10 | } 11 | 12 | export interface Student { 13 | id: string; 14 | name: string; 15 | email: string; 16 | } 17 | 18 | export type ModuleOrder = "up" | "down"; 19 | -------------------------------------------------------------------------------- /api/src/healthchecks/index.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { getConfig } from "../common/config"; 3 | 4 | const healthchecksRouter = Router(); 5 | 6 | healthchecksRouter.get("/", (req, res) => { 7 | return res.json({ 8 | message: "success", 9 | mode: getConfig("MODE"), 10 | env: getConfig("NODE_ENV"), 11 | }); 12 | }); 13 | 14 | export default healthchecksRouter; 15 | -------------------------------------------------------------------------------- /api/src/middleware/not-found.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from "express"; 2 | 3 | export const notFoundHandler = ( 4 | request: Request, 5 | response: Response, 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | next: NextFunction 8 | ) => { 9 | const message = "Resource not found"; 10 | 11 | response.status(404).send({ message }); 12 | }; 13 | -------------------------------------------------------------------------------- /api/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2021: true, 4 | node: true, 5 | }, 6 | extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"], 7 | overrides: [], 8 | parser: "@typescript-eslint/parser", 9 | parserOptions: { 10 | ecmaVersion: "latest", 11 | sourceType: "module", 12 | }, 13 | plugins: ["@typescript-eslint"], 14 | rules: {}, 15 | }; 16 | -------------------------------------------------------------------------------- /api/src/common/http-exception.ts: -------------------------------------------------------------------------------- 1 | export default class HttpException extends Error { 2 | statusCode?: number; 3 | status?: number; 4 | message: string; 5 | error: string | null; 6 | 7 | constructor(statusCode: number, message: string, error?: string) { 8 | super(message); 9 | 10 | this.statusCode = statusCode; 11 | this.message = message; 12 | this.error = error || null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /api/src/common/zod-schemas.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | export const IdSchema = z.preprocess( 4 | (value) => parseInt(value as string), 5 | z.number().min(1) 6 | ); 7 | 8 | export const EmailSchema = z.string().email(); 9 | 10 | export const BooleanQueryParamSchema = z.object({ 11 | archived: z 12 | .union([z.literal("true"), z.literal("false")]) 13 | .transform((v) => v === "true"), 14 | }); 15 | -------------------------------------------------------------------------------- /webapp/src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly VITE_API_URL: string; 5 | readonly VITE_AUTH0_DOMAIN: string; 6 | readonly VITE_AUTH0_CLIENT_ID: string; 7 | readonly VITE_AUTH0_API_AUDIENCE: string; 8 | readonly VITE_ENABLE_API_MOCKING: string; 9 | // more env variables... 10 | } 11 | 12 | interface ImportMeta { 13 | readonly env: ImportMetaEnv; 14 | } 15 | -------------------------------------------------------------------------------- /webapp/src/components/Banner.tsx: -------------------------------------------------------------------------------- 1 | function Banner() { 2 | return ( 3 |
6 |
7 |

8 | Welcome to the Konamars Learning! 9 |

10 |
11 |
12 | ); 13 | } 14 | 15 | export default Banner; 16 | -------------------------------------------------------------------------------- /api/src/courses/get-all-courses.handler.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import { db } from "../common/db"; 3 | 4 | export const getAllCoursesHandler: RequestHandler = async (req, res, next) => { 5 | try { 6 | const data = await db.course.findMany({ 7 | select: { 8 | id: true, 9 | title: true, 10 | }, 11 | }); 12 | res.json(data); 13 | } catch (error) { 14 | next(error); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /webapp/src/components/ImageFilePreview.tsx: -------------------------------------------------------------------------------- 1 | type ImageFilePreviewProps = { 2 | files?: FileList; 3 | }; 4 | 5 | function ImageFilePreview(props: ImageFilePreviewProps) { 6 | if (!props.files?.length) { 7 | return null; 8 | } 9 | 10 | const srcUrl = URL.createObjectURL(props.files[0]); 11 | 12 | return ( 13 |
14 | Preview of uploaded 15 |
16 | ); 17 | } 18 | export default ImageFilePreview; 19 | -------------------------------------------------------------------------------- /webapp/vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | import { defineConfig } from "vite"; 5 | import react from "@vitejs/plugin-react"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | server: { 10 | port: 3000, 11 | }, 12 | plugins: [react()], 13 | test: { 14 | globals: true, 15 | environment: "jsdom", 16 | setupFiles: ["./src/setup.ts"], 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /webapp/src/lib/test-utils/index.ts: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/react"; 2 | 3 | const customRender = (ui: React.ReactElement, options = {}) => 4 | render(ui, { 5 | // wrap provider(s) here if needed 6 | wrapper: ({ children }) => children, 7 | ...options, 8 | }); 9 | 10 | export * from "@testing-library/react"; 11 | export { default as userEvent } from "@testing-library/user-event"; 12 | // override render export 13 | export { customRender as render }; 14 | -------------------------------------------------------------------------------- /webapp/src/pages/ManageCourses.tsx: -------------------------------------------------------------------------------- 1 | import AllCoursesSideBar from "../components/AllCoursesSideBar"; 2 | import { Outlet } from "@tanstack/react-location"; 3 | 4 | function ManageCourses() { 5 | return ( 6 | <> 7 |
8 | 9 |
10 |
11 | 12 |
13 | 14 | ); 15 | } 16 | 17 | export default ManageCourses; 18 | -------------------------------------------------------------------------------- /webapp/src/pages/admin.tsx: -------------------------------------------------------------------------------- 1 | import AdminSideBar from "../components/AdminSideBar"; 2 | import { Outlet } from "@tanstack/react-location"; 3 | 4 | function Admin() { 5 | return ( 6 |
7 |
8 |
9 | 10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | 17 | export default Admin; 18 | -------------------------------------------------------------------------------- /api/src/index.ts: -------------------------------------------------------------------------------- 1 | import app from "./app"; 2 | import { getConfig } from "./common/config"; 3 | 4 | if (!getConfig("PORT")) { 5 | process.exit(1); 6 | } 7 | 8 | const PORT: number = parseInt(getConfig("PORT") as string, 10); 9 | 10 | async function main() { 11 | // Start the server 12 | app.listen(PORT, () => { 13 | console.log(`🚀 App started in ${getConfig("MODE")} mode on port ${PORT}.`); 14 | }); 15 | } 16 | 17 | main().catch((error) => { 18 | console.error(error); 19 | process.exit(1); 20 | }); 21 | -------------------------------------------------------------------------------- /webapp/src/components/AdminSideBar.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@tanstack/react-location"; 2 | 3 | function AdminSideBar() { 4 | return ( 5 |
6 |
7 |
8 | Courses 9 | 10 | + Add 11 | 12 |
13 |
14 |
15 | ); 16 | } 17 | 18 | export default AdminSideBar; 19 | -------------------------------------------------------------------------------- /webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Digital Lync LMS 9 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /api/src/courses/modules/delete-module.handler.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import { db } from "../../common/db"; 3 | import { IdSchema } from "../../common/zod-schemas"; 4 | 5 | export const deleteModuleHandler: RequestHandler = async (req, res, next) => { 6 | try { 7 | const moduleId = await IdSchema.parseAsync(req.params.moduleId); 8 | await db.module.delete({ 9 | where: { 10 | id: moduleId, 11 | }, 12 | }); 13 | res.sendStatus(200); 14 | } catch (error) { 15 | next(error); 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /webapp/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "react-app", 12 | "plugin:react/jsx-runtime", 13 | ], 14 | overrides: [], 15 | parser: "@typescript-eslint/parser", 16 | parserOptions: { 17 | ecmaVersion: "latest", 18 | sourceType: "module", 19 | }, 20 | plugins: ["react", "@typescript-eslint"], 21 | rules: {}, 22 | }; 23 | -------------------------------------------------------------------------------- /webapp/src/hooks/useGetUserPermissions.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import axiosInstance from "../lib/http-client"; 3 | 4 | const getUserPermissions = async () => { 5 | const response = await axiosInstance.get(`/users/permissions`); 6 | return response.data; 7 | }; 8 | 9 | export default function useGetUserPermissions(isAuthenticated: boolean) { 10 | const query = useQuery({ 11 | queryKey: ["getUserPermissions"], 12 | queryFn: getUserPermissions, 13 | enabled: isAuthenticated, 14 | }); 15 | 16 | return query; 17 | } 18 | -------------------------------------------------------------------------------- /api/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker compose start container with env variables 2 | services: 3 | local-db: 4 | image: postgres 5 | environment: 6 | - POSTGRES_USER=postgres 7 | - POSTGRES_PASSWORD=itjustworks 8 | - POSTGRES_DB=mydb 9 | ports: 10 | - "5432:5432" 11 | 12 | api-server: 13 | image: lms-public-api 14 | build: . 15 | environment: 16 | - MODE=local 17 | - PORT=8080 18 | - DATABASE_URL=postgresql://postgres:itjustworks@local-db:5432/mydb 19 | ports: 20 | - "8080:8080" 21 | depends_on: 22 | - local-db 23 | -------------------------------------------------------------------------------- /webapp/src/hooks/useGetCourse.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import axiosInstance from "../lib/http-client"; 3 | import { Course } from "../types/courses"; 4 | 5 | export const getCourse = async (courseId: string) => { 6 | try { 7 | const response = await axiosInstance.get(`/courses/${courseId}`); 8 | return response.data; 9 | } catch (error) { 10 | console.log(error); 11 | } 12 | }; 13 | 14 | export default function useGetCourse(courseId: string) { 15 | return useQuery({ 16 | queryKey: ["getCourse", courseId], 17 | queryFn: () => getCourse(courseId), 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /webapp/src/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import { rest } from "msw"; 2 | 3 | // Mock Data 4 | import { courseData, courses } from "./mock-data/courses"; 5 | 6 | // Define handlers that catch the corresponding requests and returns the mock data. 7 | export const handlers = [ 8 | rest.get( 9 | `${import.meta.env.VITE_API_URL}/student/courses`, 10 | (req, res, ctx) => { 11 | return res(ctx.status(200), ctx.json(courses)); 12 | } 13 | ), 14 | rest.get( 15 | `${import.meta.env.VITE_API_URL}/student/getCourse/:id`, 16 | (req, res, ctx) => { 17 | return res(ctx.status(200), ctx.json(courseData)); 18 | } 19 | ), 20 | ]; 21 | -------------------------------------------------------------------------------- /webapp/src/lib/react-query.ts: -------------------------------------------------------------------------------- 1 | import { QueryClient } from "@tanstack/react-query"; 2 | import { toast } from "react-toastify"; 3 | 4 | const apiErrorHandler = (err: unknown) => { 5 | const error = err as { message: string }; 6 | if (error.message) { 7 | toast(error.message); 8 | } else { 9 | toast("Unknow error, please contact support."); 10 | } 11 | }; 12 | 13 | export const queryClient = new QueryClient({ 14 | defaultOptions: { 15 | queries: { 16 | retry: false, 17 | refetchOnWindowFocus: false, 18 | onError: apiErrorHandler, 19 | }, 20 | mutations: { 21 | onError: apiErrorHandler, 22 | }, 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src", "src/pages/.tsx"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /webapp/src/hooks/useGetCourses.ts: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | import axiosInstance from "../lib/http-client"; 4 | import { CourseListItem } from "../types/courses"; 5 | 6 | export const getCourses = async () => { 7 | try { 8 | const response = await axiosInstance.get("courses"); 9 | return response.data; 10 | } catch (error) { 11 | if (axios.isAxiosError(error)) { 12 | return Promise.reject(error.response?.data); 13 | } 14 | } 15 | }; 16 | 17 | export default function useGetCourses() { 18 | return useQuery({ 19 | queryKey: ["getCourses"], 20 | queryFn: getCourses, 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /api/src/courses/get-my-courses.handler.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import { db } from "../common/db"; 3 | 4 | export const getEnrolledCourses = async () => { 5 | const data = await db.course.findMany({ 6 | where: { 7 | archived: false, 8 | }, 9 | select: { 10 | id: true, 11 | title: true, 12 | description: true, 13 | liveLink: true, 14 | }, 15 | }); 16 | 17 | return data; 18 | }; 19 | 20 | export const getMyCoursesHandler: RequestHandler = async (req, res, next) => { 21 | try { 22 | const data = await getEnrolledCourses(); 23 | return res.json(data); 24 | } catch (error) { 25 | next(error); 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /webapp/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { Outlet, ReactLocation, Router } from "@tanstack/react-location"; 3 | import { LocationGenerics, routes } from "./routes"; 4 | import MainLayout from "./layouts/MainLayout"; 5 | import { ToastContainer } from "react-toastify"; 6 | import "react-toastify/dist/ReactToastify.min.css"; 7 | 8 | const location = new ReactLocation(); 9 | 10 | function App() { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | 23 | export default App; 24 | -------------------------------------------------------------------------------- /webapp/src/components/Resources.tsx: -------------------------------------------------------------------------------- 1 | type ResourcesProps = { 2 | resourceFiles?: string[]; 3 | }; 4 | 5 | function Resources(props: ResourcesProps) { 6 | if (!props.resourceFiles?.length) { 7 | return ( 8 |
9 |

No resource available for this topic.

10 |
11 | ); 12 | } 13 | 14 | return ( 15 |
16 |
    17 | {props.resourceFiles.map((file, index) => ( 18 |
  • 19 | 20 | {file} 21 | 22 |
  • 23 | ))} 24 |
25 |
26 | ); 27 | } 28 | export default Resources; 29 | -------------------------------------------------------------------------------- /webapp/src/hooks/useCourseFilesUpdateMutation.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | import axiosInstance from "../lib/http-client"; 4 | 5 | function useCourseFilesUpdateMutation() { 6 | return useMutation({ 7 | mutationKey: ["updateCourseFiles"], 8 | mutationFn: async (data: FormData) => { 9 | try { 10 | const res = await axiosInstance.put("/courses/update-files", data); 11 | return res.data; 12 | } catch (error) { 13 | if (axios.isAxiosError(error)) { 14 | return Promise.reject(error.response?.data); 15 | } 16 | } 17 | }, 18 | }); 19 | } 20 | 21 | export default useCourseFilesUpdateMutation; 22 | -------------------------------------------------------------------------------- /api/src/middleware/error.middleware.ts: -------------------------------------------------------------------------------- 1 | import HttpException from "../common/http-exception"; 2 | import { Request, Response, NextFunction } from "express"; 3 | import { z } from "zod"; 4 | 5 | export const errorHandler = ( 6 | error: HttpException, 7 | req: Request, 8 | res: Response, 9 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 10 | next: NextFunction 11 | ) => { 12 | console.log("🚀 ~ file: error.middleware.ts ~ line 13 ~ error", error); 13 | const status = error.statusCode || error.status || 500; 14 | 15 | if (error instanceof z.ZodError) { 16 | return res.status(400).json({ 17 | message: "Invalid request", 18 | }); 19 | } 20 | 21 | res.status(status).send(error); 22 | }; 23 | -------------------------------------------------------------------------------- /webapp/tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [ 8 | require("@tailwindcss/line-clamp"), 9 | require("@tailwindcss/typography"), 10 | require("daisyui"), 11 | ], 12 | daisyui: { 13 | // themes: [ 14 | // { 15 | // light: { 16 | // // eslint-disable-next-line @typescript-eslint/no-var-requires 17 | // ...require("daisyui/src/colors/themes")["[data-theme=corporate]"], 18 | // primary: "#F4B400", 19 | // "primary-focus": "#ffc31a", 20 | // }, 21 | // }, 22 | // ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /api/src/courses/archieve-course-handler.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import { z } from "zod"; 3 | import { db } from "../common/db"; 4 | import { IdSchema } from "../common/zod-schemas"; 5 | 6 | export const archiveCourseHandler: RequestHandler = async (req, res, next) => { 7 | try { 8 | const courseId = IdSchema.parse(req.params.courseId); 9 | const archived = z.boolean().parse(req.body.archived); 10 | 11 | // Update the course in the database 12 | await db.course.update({ 13 | where: { 14 | id: courseId, 15 | }, 16 | data: { 17 | archived, 18 | }, 19 | }); 20 | 21 | res.sendStatus(204); 22 | } catch (error) { 23 | next(error); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /webapp/src/hooks/useGetAllCourses.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | import axiosInstance from "../lib/http-client"; 4 | import { CourseNavListItem } from "../types/courses"; 5 | 6 | export const getAllCourses = async () => { 7 | try { 8 | const response = await axiosInstance.get( 9 | "/courses/all" 10 | ); 11 | return response.data; 12 | } catch (error) { 13 | if (axios.isAxiosError(error)) { 14 | return Promise.reject(error.response?.data); 15 | } 16 | } 17 | }; 18 | 19 | export default function useGetAllCourses() { 20 | return useQuery({ 21 | queryKey: ["getAllCourses"], 22 | queryFn: getAllCourses, 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /api/src/courses/update-live-link.handler.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import { z } from "zod"; 3 | import { db } from "../common/db"; 4 | import { IdSchema } from "../common/zod-schemas"; 5 | 6 | export const updateLiveLinkHandler: RequestHandler = async (req, res, next) => { 7 | try { 8 | const courseId = IdSchema.parse(req.params.courseId); 9 | const liveLink = z.string().url().parse(req.body.liveLink); 10 | 11 | // Update the course in the database 12 | await db.course.update({ 13 | where: { 14 | id: courseId, 15 | }, 16 | data: { 17 | liveLink, 18 | }, 19 | }); 20 | 21 | res.sendStatus(204); 22 | } catch (error) { 23 | next(error); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /webapp/src/components/Assignments.tsx: -------------------------------------------------------------------------------- 1 | type AssignmentsProps = { 2 | assignmentFiles?: string[]; 3 | }; 4 | 5 | function Assignments(props: AssignmentsProps) { 6 | if (!props.assignmentFiles?.length) { 7 | return ( 8 |
9 |

No assignments available for this topic.

10 |
11 | ); 12 | } 13 | 14 | return ( 15 |
16 |
    17 | {props.assignmentFiles.map((file, index) => ( 18 |
  • 19 | 20 | {file} 21 | 22 |
  • 23 | ))} 24 |
25 |
26 | ); 27 | } 28 | 29 | export default Assignments; 30 | -------------------------------------------------------------------------------- /api/src/courses/modules/module.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { createModuleHandler } from "./create-module.handler"; 3 | import { deleteModuleHandler } from "./delete-module.handler"; 4 | import { updateModuleOrderHandler } from "./update-module-order"; 5 | import { updateModuleHandler } from "./update-module.handler"; 6 | 7 | const moduleRouter = Router({ 8 | mergeParams: true, // If this is not true params from parent router are dropped. 9 | // https://expressjs.com/en/4x/api.html#express.router 10 | }); 11 | 12 | moduleRouter.post("/", createModuleHandler); 13 | 14 | moduleRouter 15 | .route("/:moduleId") 16 | .delete(deleteModuleHandler) 17 | .put(updateModuleHandler) 18 | .patch(updateModuleOrderHandler); 19 | 20 | export default moduleRouter; 21 | -------------------------------------------------------------------------------- /webapp/src/components/Projects.tsx: -------------------------------------------------------------------------------- 1 | import { getFileNameFromBlobUrl } from "../lib/strings"; 2 | 3 | type ProjectsProps = { 4 | projectFiles: string[]; 5 | }; 6 | 7 | function Projects(props: ProjectsProps) { 8 | if (!props.projectFiles?.length) { 9 | return ( 10 |
11 |

No projects available for this course.

12 |
13 | ); 14 | } 15 | 16 | return ( 17 |
18 | 27 |
28 | ); 29 | } 30 | export default Projects; 31 | -------------------------------------------------------------------------------- /api/src/app.ts: -------------------------------------------------------------------------------- 1 | import express from "express"; 2 | import cors from "cors"; 3 | import helmet from "helmet"; 4 | import { errorHandler } from "./middleware/error.middleware"; 5 | import { notFoundHandler } from "./middleware/not-found.middleware"; 6 | import morgan from "morgan"; 7 | import healthchecksRouter from "./healthchecks"; 8 | import coursesRouter from "./courses/courses.router"; 9 | 10 | const app = express(); 11 | 12 | /** 13 | * Middlewares 14 | */ 15 | app.use(morgan("dev")); 16 | app.use(helmet()); 17 | 18 | // Cors allow all origins 19 | app.use(cors()); 20 | app.use(express.json()); 21 | 22 | // Routers 23 | app.use("/api", healthchecksRouter); 24 | app.use("/api/courses", coursesRouter); 25 | 26 | // Error and 404 27 | app.use(errorHandler); 28 | app.use(notFoundHandler); 29 | 30 | export default app; 31 | -------------------------------------------------------------------------------- /webapp/Dockerfile: -------------------------------------------------------------------------------- 1 | # Docker file for react app created using vite 2 | FROM node:latest as build 3 | # Create app directory 4 | WORKDIR /app 5 | # Install app dependencies 6 | COPY package*.json ./ 7 | RUN npm install 8 | # Build app 9 | COPY . . 10 | RUN npm run build 11 | 12 | # Docker file for nginx server to serve react app build files (spa routing) 13 | FROM nginx:alpine 14 | COPY --from=build /app/dist /usr/share/nginx/html 15 | # This is required to make nginx serve index.html for all routes 16 | COPY nginx.conf /etc/nginx/conf.d/default.conf 17 | EXPOSE 80 18 | # If you add a custom CMD in the Dockerfile, be sure to include -g daemon off; 19 | # in the CMD in order for nginx to stay in the foreground, so that Docker can 20 | # track the process properly (otherwise your container will stop immediately after starting)! 21 | CMD ["nginx", "-g", "daemon off;"] 22 | -------------------------------------------------------------------------------- /webapp/src/lib/strings.ts: -------------------------------------------------------------------------------- 1 | export function buildURL(location: string, params?: Record) { 2 | const url = new URL(location); 3 | 4 | for (const key in params) { 5 | if (Object.prototype.hasOwnProperty.call(params, key)) { 6 | const value = params[key]; 7 | if (value) { 8 | url.searchParams.set(key, value); 9 | } 10 | } 11 | } 12 | 13 | return url.href; 14 | } 15 | 16 | export function isUrlValid(text: string) { 17 | const res = text.match( 18 | /(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/g 19 | ); 20 | if (res == null) return false; 21 | else return true; 22 | } 23 | 24 | export const getFileNameFromBlobUrl = (blobUrl: string) => { 25 | const url = new URL(blobUrl); 26 | const path = url.pathname; 27 | const fileName = path.substring(path.lastIndexOf("/") + 1); 28 | return fileName; 29 | }; 30 | -------------------------------------------------------------------------------- /api/src/courses/update-course.handler.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import { z } from "zod"; 3 | import { db } from "../common/db"; 4 | import { IdSchema } from "../common/zod-schemas"; 5 | 6 | const updateCourseSchema = z.object({ 7 | archived: z.boolean(), 8 | description: z.string().min(1), 9 | liveLink: z.string().url(), 10 | pictrue: z.string().url(), 11 | title: z.string().min(1), 12 | }); 13 | 14 | export const updateCourseHandler: RequestHandler = async (req, res, next) => { 15 | try { 16 | // Validate data 17 | const courseId = IdSchema.parse(req.params.courseId); 18 | const data = updateCourseSchema.parse(req.body); 19 | 20 | // Update in db 21 | await db.course.update({ 22 | where: { 23 | id: courseId, 24 | }, 25 | data: data, 26 | }); 27 | 28 | return res.sendStatus(200); 29 | } catch (error) { 30 | next(error); 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /api/src/courses/create-course.handler.ts: -------------------------------------------------------------------------------- 1 | import { RequestHandler } from "express"; 2 | import { z } from "zod"; 3 | import { db } from "../common/db"; 4 | 5 | export const createCourseSchema = z.object({ 6 | title: z.string().min(1), 7 | description: z.string().min(1), 8 | archived: z.preprocess( 9 | (value) => value === "true", 10 | z.boolean().default(false) 11 | ), 12 | }); 13 | 14 | export const createCourseHandler: RequestHandler = async (req, res, next) => { 15 | try { 16 | console.log( 17 | "🚀 ~ file: create-course.handler.ts ~ line 17 ~ constcreateCourseHandler:RequestHandler= ~ req.body", 18 | req.body 19 | ); 20 | const data = await createCourseSchema.parseAsync(req.body); 21 | const course = await db.course.create({ 22 | data, 23 | }); 24 | res.status(201).json({ 25 | id: course.id, 26 | title: course.title, 27 | }); 28 | } catch (error) { 29 | next(error); 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /webapp/src/hooks/verifyEmailMutation.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | import { toast } from "react-toastify"; 4 | import axiosInstance from "../lib/http-client"; 5 | 6 | function useVerifyEmailMutation() { 7 | return useMutation({ 8 | mutationKey: ["verifyEmail"], 9 | mutationFn: async () => { 10 | try { 11 | const res = await axiosInstance.post("/users/verify-email"); 12 | return res.data; 13 | } catch (error) { 14 | if (axios.isAxiosError(error)) { 15 | return Promise.reject(error.response?.data); 16 | } 17 | } 18 | }, 19 | onSuccess: () => { 20 | toast("Verification email sent.", { 21 | type: "success", 22 | }); 23 | }, 24 | onError: () => { 25 | toast("Failed to send verification email", { 26 | type: "error", 27 | }); 28 | }, 29 | }); 30 | } 31 | 32 | export default useVerifyEmailMutation; 33 | -------------------------------------------------------------------------------- /webapp/src/hooks/usePasswordResetMutation.ts: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@tanstack/react-query"; 2 | import axios from "axios"; 3 | import { toast } from "react-toastify"; 4 | import axiosInstance from "../lib/http-client"; 5 | 6 | function usePasswordResetMutation() { 7 | return useMutation({ 8 | mutationKey: ["passwordReset"], 9 | mutationFn: async () => { 10 | try { 11 | const res = await axiosInstance.post("/users/reset-password"); 12 | return res.data; 13 | } catch (error) { 14 | if (axios.isAxiosError(error)) { 15 | return Promise.reject(error.response?.data); 16 | } 17 | } 18 | }, 19 | onSuccess: () => { 20 | toast("Password reset email sent.", { 21 | type: "success", 22 | }); 23 | }, 24 | onError: () => { 25 | toast("Failed to send password reset link.", { 26 | type: "error", 27 | }); 28 | }, 29 | }); 30 | } 31 | 32 | export default usePasswordResetMutation; 33 | -------------------------------------------------------------------------------- /webapp/src/components/NoCourses.tsx: -------------------------------------------------------------------------------- 1 | function NoCourses() { 2 | return ( 3 |
4 |

{"There's nothing here..."}

5 | 6 |

7 | Enrolled courses will appear here, click below button to contact the 8 | academy. 9 |

10 | 11 | 28 |
29 | ); 30 | } 31 | 32 | export default NoCourses; 33 | -------------------------------------------------------------------------------- /webapp/README.md: -------------------------------------------------------------------------------- 1 | # Konamars LMS 2 | 3 | ## App environments 4 | 5 | | Environment | Git Branch | 6 | | ------------------------------------------------------------------------- | ---------- | 7 | | [production](https://calm-pebble-0c3131110.1.azurestaticapps.net/) | main | 8 | | [dev](https://calm-pebble-0c3131110-dev.centralus.1.azurestaticapps.net/) | dev | 9 | | [qa](https://calm-pebble-0c3131110-dev.centralus.1.azurestaticapps.net/) | qa | 10 | 11 | ## Run project locally 12 | 13 | - Install node modules with `npm install` 14 | - Run local dev server with `npm dev` 15 | 16 | ## Run in production 17 | 18 | - Install node modules with `npm install` 19 | - Build with `npm build` 20 | - Serve generated `dist/` folder with a production web server 21 | 22 | _View the deployment resource in azure cloud portal at [here.](https://portal.azure.com/#@mkonakonamars.onmicrosoft.com/resource/subscriptions/d5f3450e-23c9-47f0-a07c-f650dee64c3c/resourcegroups/javascript-stack/providers/Microsoft.Web/staticSites/konamars/staticsite)_ 23 | -------------------------------------------------------------------------------- /webapp/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import "./index.css"; 5 | import { QueryClientProvider } from "@tanstack/react-query"; 6 | import { queryClient } from "./lib/react-query"; 7 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 8 | 9 | async function main() { 10 | console.log({ 11 | PROD: import.meta.env.PROD, 12 | MODE: import.meta.env.MODE, 13 | VITE_API_URL: import.meta.env.VITE_API_URL, 14 | }); 15 | 16 | if (import.meta.env.VITE_ENABLE_API_MOCKING) { 17 | const { worker } = await import("./mocks/browser"); 18 | await worker.start({ 19 | serviceWorker: { 20 | url: "mockServiceWorker.js", 21 | }, 22 | }); 23 | } 24 | 25 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 26 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | } 34 | 35 | main(); 36 | -------------------------------------------------------------------------------- /api/src/courses/courses.router.ts: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { createCourseHandler } from "./create-course.handler"; 3 | import { getAllCoursesHandler } from "./get-all-courses.handler"; 4 | import { getCourseHandler } from "./get-course.handler"; 5 | import { getMyCoursesHandler } from "./get-my-courses.handler"; 6 | import moduleRouter from "./modules/module.router"; 7 | import { archiveCourseHandler } from "./archieve-course-handler"; 8 | import { updateCourseHandler } from "./update-course.handler"; 9 | import { updateLiveLinkHandler } from "./update-live-link.handler"; 10 | 11 | const coursesRouter = Router(); 12 | 13 | coursesRouter.route("/").post(createCourseHandler).get(getMyCoursesHandler); 14 | 15 | coursesRouter.get("/all", getAllCoursesHandler); 16 | 17 | coursesRouter 18 | .route("/:courseId") 19 | .get(getCourseHandler) 20 | .put(updateCourseHandler); 21 | 22 | coursesRouter.patch("/:courseId/archieved", archiveCourseHandler); 23 | 24 | coursesRouter.patch("/:courseId/live-link", updateLiveLinkHandler); 25 | 26 | // Modules 27 | coursesRouter.use("/:courseId/modules", moduleRouter); 28 | 29 | export default coursesRouter; 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # Docker compose start container with env variables 2 | services: 3 | local-db: 4 | image: postgres 5 | environment: 6 | - POSTGRES_USER=postgres 7 | - POSTGRES_PASSWORD=itjustworks 8 | - POSTGRES_DB=mydb 9 | ports: 10 | - "5432:5432" 11 | # volumes: # uncomment to persist data using bind mount 12 | # - ./data:/var/lib/postgresql/data 13 | # volumes: # uncomment to persist data using named volume 14 | # - local-db-data:/var/lib/postgresql/data 15 | 16 | api-server: 17 | image: lms-public-api 18 | build: api/ 19 | environment: 20 | - MODE=local 21 | - PORT=8080 22 | - DATABASE_URL=postgresql://postgres:itjustworks@local-db:5432/mydb 23 | ports: 24 | - "8080:8080" 25 | restart: on-failure 26 | depends_on: 27 | - local-db 28 | 29 | web-server: 30 | image: lms-web 31 | build: webapp/ 32 | environment: 33 | - MODE=local 34 | - API_URL=http://api-server:8080 35 | ports: 36 | - "3000:80" 37 | depends_on: 38 | - api-server 39 | # volumes: 40 | # local-db-data: # uncomment to persist data using named volume 41 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent any 3 | environment { 4 | // More detail: 5 | // https://jenkins.io/doc/book/pipeline/jenkinsfile/#usernames-and-passwords 6 | NEXUS_CRED = credentials('nexus') 7 | } 8 | 9 | stages { 10 | stage('Build') { 11 | steps { 12 | echo 'Building..' 13 | sh 'cd webapp && npm install && npm run build' 14 | } 15 | } 16 | stage('Test') { 17 | steps { 18 | echo 'Testing..' 19 | sh 'cd webapp && sudo docker container run --rm -e SONAR_HOST_URL="http://20.172.187.108:9000" -e SONAR_LOGIN="sqp_cae41e62e13793ff17d58483fb6fb82602fe2b48" -v ".:/usr/src" sonarsource/sonar-scanner-cli -Dsonar.projectKey=lms' 20 | } 21 | } 22 | stage('Release') { 23 | steps { 24 | echo 'Release Nexus' 25 | sh 'rm -rf *.zip' 26 | sh 'cd webapp && zip dist-${BUILD_NUMBER}.zip -r dist' 27 | sh 'cd webapp && curl -v -u $Username:$Password --upload-file dist-${BUILD_NUMBER}.zip http://20.172.187.108:8081/repository/lms/' 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /webapp/src/types/courses.ts: -------------------------------------------------------------------------------- 1 | export type Tabs = 2 | | "Course Info" 3 | | "Resources" 4 | | "Assignments" 5 | | "Projects" 6 | | "Enrolled" 7 | | "Modules"; 8 | 9 | export interface CourseListItem { 10 | id: number; 11 | title: string; 12 | description: string; 13 | pictrue: string; 14 | liveLink?: string; 15 | } 16 | 17 | export interface Course { 18 | id: number; 19 | title: string; 20 | description: string; 21 | pictrue: string; 22 | liveLink: string; 23 | modules: Module[]; 24 | modulesOrder: number[]; 25 | projectFiles: string[]; 26 | archived: boolean; 27 | } 28 | 29 | export interface CourseNavListItem { 30 | id: number; 31 | title: string; 32 | } 33 | export interface ModuleNavListItem { 34 | id: number; 35 | title: string; 36 | } 37 | 38 | export interface Module { 39 | id: number; 40 | title: string; 41 | topics: Topic[]; 42 | } 43 | 44 | export interface Topic { 45 | id: number; 46 | title: string; 47 | videoLink: string; 48 | resourceFiles: string[]; 49 | assignmentFiles: string[]; 50 | } 51 | 52 | export interface TopicInput { 53 | title: string; 54 | videoLink: string; 55 | id: number | string; 56 | } 57 | 58 | export interface ModuleInput { 59 | title: string; 60 | topics: TopicInput[]; 61 | } 62 | -------------------------------------------------------------------------------- /webapp/src/components/VimeoVideo.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from "react"; 2 | import { buildURL } from "../lib/strings"; 3 | import "./vimeo-video.css"; 4 | 5 | type VimeoVideoProps = { 6 | videoId: string; 7 | params?: Record; 8 | width?: number; 9 | height?: number; 10 | }; 11 | 12 | function VimeoVideo(props: VimeoVideoProps) { 13 | const playerIframeRef = useRef(null); 14 | // let player = new Vimeo.Player(playerIframeRef); 15 | useEffect(() => { 16 | if (!playerIframeRef.current) { 17 | return; 18 | } 19 | // const player = new vimeo.Player(playerIframeRef.current); 20 | // player.on("play", (event) => { 21 | // console.log("music started..."); 22 | // console.log(event); 23 | // }); 24 | }, []); 25 | 26 | return ( 27 |
28 |