From a29f9011049916977cb089a663bc20b9dbcc8752 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 21 Jan 2026 16:24:37 +0000 Subject: [PATCH] test --- backend/src/db/db.config.js | 11 +- ...20260121000000-grant-public-permissions.js | 19 + backend/src/index.js | 2 +- frontend/next.config.mjs | 1 - frontend/src/components/NavBarItem.tsx | 3 +- frontend/src/layouts/Authenticated.tsx | 3 +- frontend/src/pages/course/[id].tsx | 289 +++++++++++ frontend/src/pages/dashboard.tsx | 466 +++++------------- frontend/src/pages/index.tsx | 346 +++++++------ 9 files changed, 654 insertions(+), 486 deletions(-) create mode 100644 backend/src/db/migrations/20260121000000-grant-public-permissions.js create mode 100644 frontend/src/pages/course/[id].tsx diff --git a/backend/src/db/db.config.js b/backend/src/db/db.config.js index b353587..cfe8a2f 100644 --- a/backend/src/db/db.config.js +++ b/backend/src/db/db.config.js @@ -1,5 +1,3 @@ - - module.exports = { production: { dialect: 'postgres', @@ -12,11 +10,12 @@ module.exports = { seederStorage: 'sequelize', }, development: { - username: 'postgres', dialect: 'postgres', - password: '', - database: 'db_instructor_student_lms', + username: process.env.DB_USER || 'postgres', + password: process.env.DB_PASS || '', + database: process.env.DB_NAME || 'db_instructor_student_lms', host: process.env.DB_HOST || 'localhost', + port: process.env.DB_PORT || 5432, logging: console.log, seederStorage: 'sequelize', }, @@ -30,4 +29,4 @@ module.exports = { logging: console.log, seederStorage: 'sequelize', } -}; +}; \ No newline at end of file diff --git a/backend/src/db/migrations/20260121000000-grant-public-permissions.js b/backend/src/db/migrations/20260121000000-grant-public-permissions.js new file mode 100644 index 0000000..e4137f1 --- /dev/null +++ b/backend/src/db/migrations/20260121000000-grant-public-permissions.js @@ -0,0 +1,19 @@ +module.exports = { + async up(queryInterface, Sequelize) { + await queryInterface.sequelize.query(` + INSERT INTO "rolesPermissionsPermissions" ("createdAt", "updatedAt", "roles_permissionsId", "permissionId") + SELECT NOW(), NOW(), r.id, p.id + FROM roles r, permissions p + WHERE r.name = 'Public' AND p.name IN ('READ_COURSES', 'READ_USERS', 'READ_CATEGORIES', 'READ_LESSONS') + ON CONFLICT ("roles_permissionsId", "permissionId") DO NOTHING; + `); + }, + + async down(queryInterface, Sequelize) { + await queryInterface.sequelize.query(` + DELETE FROM "rolesPermissionsPermissions" + WHERE "roles_permissionsId" IN (SELECT id FROM roles WHERE name = 'Public') + AND "permissionId" IN (SELECT id FROM permissions WHERE name IN ('READ_COURSES', 'READ_USERS', 'READ_CATEGORIES', 'READ_LESSONS')); + `); + } +}; \ No newline at end of file diff --git a/backend/src/index.js b/backend/src/index.js index 9f7e17c..9a55a18 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -103,7 +103,7 @@ app.use('/api/roles', passport.authenticate('jwt', {session: false}), rolesRoute app.use('/api/permissions', passport.authenticate('jwt', {session: false}), permissionsRoutes); -app.use('/api/courses', passport.authenticate('jwt', {session: false}), coursesRoutes); +app.use('/api/courses', coursesRoutes); app.use('/api/lessons', passport.authenticate('jwt', {session: false}), lessonsRoutes); diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 89767ec..77baedf 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -26,7 +26,6 @@ trailingSlash: true, }, ], }, - } export default nextConfig \ No newline at end of file diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx index 72935e6..995a452 100644 --- a/frontend/src/components/NavBarItem.tsx +++ b/frontend/src/components/NavBarItem.tsx @@ -1,6 +1,5 @@ -import React, {useEffect, useRef} from 'react' +import React, {useEffect, useRef, useState} from 'react' import Link from 'next/link' -import { useState } from 'react' import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import BaseDivider from './BaseDivider' import BaseIcon from './BaseIcon' diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx index 1b9907d..73d8391 100644 --- a/frontend/src/layouts/Authenticated.tsx +++ b/frontend/src/layouts/Authenticated.tsx @@ -1,5 +1,4 @@ -import React, { ReactNode, useEffect } from 'react' -import { useState } from 'react' +import React, { ReactNode, useEffect, useState } from 'react' import jwt from 'jsonwebtoken'; import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import menuAside from '../menuAside' diff --git a/frontend/src/pages/course/[id].tsx b/frontend/src/pages/course/[id].tsx new file mode 100644 index 0000000..894f283 --- /dev/null +++ b/frontend/src/pages/course/[id].tsx @@ -0,0 +1,289 @@ +import React, { useEffect, useState } from 'react'; +import type { ReactElement } from 'react'; +import Head from 'next/head'; +import { useRouter } from 'next/router'; +import axios from 'axios'; +import Link from 'next/link'; +import LayoutGuest from '../../layouts/Guest'; +import { useAppSelector } from '../../stores/hooks'; +import { mdiClockOutline, mdiAccount, mdiTagOutline, mdiTranslate, mdiArrowLeft } from '@mdi/js'; +import BaseIcon from '../../components/BaseIcon'; +import BaseButton from '../../components/BaseButton'; + +// Course Details Page +export default function CourseDetailsPage() { + const router = useRouter(); + const { id } = router.query; + const projectName = useAppSelector((state) => state.style.projectName) || 'EduFlow'; + + const [course, setCourse] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + // Wait for router to be ready and id to be available + if (!router.isReady) return; + + if (id && typeof id === 'string') { + const fetchCourse = async () => { + try { + setLoading(true); + console.log('Fetching course with ID:', id); + const response = await axios.get(`/courses/${id}`); + setCourse(response.data); + setError(null); + } catch (err) { + console.error('Failed to fetch course:', err); + setError(err); + } finally { + setLoading(false); + } + }; + fetchCourse(); + } else { + // If router is ready but no ID, stop loading + console.log('Router ready but no ID found in query:', router.query); + setLoading(false); + } + }, [id, router.isReady]); + + const Header = () => ( +
+
+ + {projectName} + +
+ + All Courses + + +
+
+
+ ); + + const Footer = () => ( +
+
+
+
+ {projectName} +

Empowering learners worldwide.

+
+
+ © 2026 {projectName} LMS. All rights reserved. +
+
+
+
+ ); + + if (loading) { + return ( +
+
+
+
+
+

Loading course details...

+
+
+
+
+ ); + } + + if (error || !course) { + return ( +
+
+
+
+

Course not found

+

The course you are looking for might have been removed or the link is incorrect.

+ router.push('/')} icon={mdiArrowLeft} /> +
+
+
+
+ ); + } + + const getThumbnail = (course: any) => { + if (course.thumbnail && course.thumbnail.length > 0) { + return `/api/file/download?privateUrl=${course.thumbnail[0].privateUrl}`; + } + return 'https://images.pexels.com/photos/1181244/pexels-photo-1181244.jpeg?auto=compress&cs=tinysrgb&w=600'; + }; + + return ( +
+ + {`${course.title} | ${projectName}`} + +
+ +
+ {/* Hero Section */} +
+
+
+
+ {course.category && ( + + {course.category.name} + + )} + + {course.level || 'Beginner'} + +
+

+ {course.title} +

+

+ {course.short_description} +

+ +
+
+ + By {course.instructor ? `${course.instructor.firstName}${course.instructor.lastName ? " " + course.instructor.lastName : ""}` : 'Instructor'} +
+ {course.duration && ( +
+ + {course.duration} minutes +
+ )} + {course.language && ( +
+ + {course.language} +
+ )} +
+
+
+
+ +
+
+ {/* Main Content */} +
+
+

Course Description

+
+ {course.description || 'No description available.'} +
+
+ +
+
+

Curriculum

+ + {course.lessons_course?.length || 0} lessons + +
+ +
+ {course.lessons_course && course.lessons_course.length > 0 ? ( + course.lessons_course.sort((a: any, b: any) => (a.order || 0) - (b.order || 0)).map((lesson: any, index: number) => ( +
+
+ {index + 1} +
+
+

{lesson.title}

+

{lesson.content || lesson.short_description || 'No content description.'}

+
+ {lesson.duration && ( +
+ {lesson.duration} min +
+ )} +
+ )) + ) : ( +
+ +

+ No lessons available for this course yet. +

+
+ )} +
+
+
+ + {/* Sidebar */} +
+
+
+ {course.title} +
+
+ +
+
+ + {course.price === 0 || !course.price || course.price === '0' ? 'Free' : `$${course.price}`} + + {course.price > 0 && ( + $349 + )} +
+ + +

+ 30-Day Money-Back Guarantee +

+
+ +
+

Course Features

+
    +
  • +
    + +
    + {course.duration ? `${course.duration} minutes total` : 'Flexible duration'} +
  • +
  • +
    + +
    + Direct instructor access +
  • +
  • +
    + +
    + Certificate of completion +
  • +
+
+
+
+
+
+
+ +
+
+ ); +} + +CourseDetailsPage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; \ No newline at end of file diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 864b468..a58ebdc 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -9,6 +9,8 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton import BaseIcon from "../components/BaseIcon"; import { getPageTitle } from '../config' import Link from "next/link"; +import CardBox from '../components/CardBox'; +import BaseButton from '../components/BaseButton'; import { hasPermission } from "../helpers/userPermissions"; import { fetchWidgets } from '../stores/roles/rolesSlice'; @@ -16,48 +18,41 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator'; import { SmartWidget } from '../components/SmartWidget/SmartWidget'; import { useAppDispatch, useAppSelector } from '../stores/hooks'; + const Dashboard = () => { const dispatch = useAppDispatch(); const iconsColor = useAppSelector((state) => state.style.iconsColor); const corners = useAppSelector((state) => state.style.corners); const cardsStyle = useAppSelector((state) => state.style.cardsStyle); - const loadingMessage = 'Loading...'; - + const loadingMessage = '...'; const [users, setUsers] = React.useState(loadingMessage); const [roles, setRoles] = React.useState(loadingMessage); - const [permissions, setPermissions] = React.useState(loadingMessage); const [courses, setCourses] = React.useState(loadingMessage); const [lessons, setLessons] = React.useState(loadingMessage); const [enrollments, setEnrollments] = React.useState(loadingMessage); const [progress, setProgress] = React.useState(loadingMessage); const [categories, setCategories] = React.useState(loadingMessage); - const [resources, setResources] = React.useState(loadingMessage); - const [widgetsRole, setWidgetsRole] = React.useState({ role: { value: '', label: '' }, }); const { currentUser } = useAppSelector((state) => state.auth); const { isFetchingQuery } = useAppSelector((state) => state.openAi); - const { rolesWidgets, loading } = useAppSelector((state) => state.roles); - - + async function loadData() { - const entities = ['users','roles','permissions','courses','lessons','enrollments','progress','categories','resources',]; - const fns = [setUsers,setRoles,setPermissions,setCourses,setLessons,setEnrollments,setProgress,setCategories,setResources,]; + const entities = ['users', 'roles', 'courses', 'lessons', 'enrollments', 'progress', 'categories']; + const fns = [setUsers, setRoles, setCourses, setLessons, setEnrollments, setProgress, setCategories]; const requests = entities.map((entity, index) => { - - if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { - return axios.get(`/${entity.toLowerCase()}/count`); - } else { - fns[index](null); - return Promise.resolve({data: {count: null}}); - } - + if (hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) { + return axios.get(`/${entity.toLowerCase()}/count`); + } else { + fns[index](null); + return Promise.resolve({ data: { count: null } }); + } }); Promise.allSettled(requests).then((results) => { @@ -65,15 +60,12 @@ const Dashboard = () => { if (result.status === 'fulfilled') { fns[i](result.value.data.count); } else { - fns[i](result.reason.message); + fns[i](0); } }); }); } - - async function getWidgets(roleId) { - await dispatch(fetchWidgets(roleId)); - } + React.useEffect(() => { if (!currentUser) return; loadData().then(); @@ -82,329 +74,133 @@ const Dashboard = () => { React.useEffect(() => { if (!currentUser || !widgetsRole?.role?.value) return; - getWidgets(widgetsRole?.role?.value || '').then(); + dispatch(fetchWidgets(widgetsRole?.role?.value || '')).then(); }, [widgetsRole?.role?.value]); - - return ( - <> - - - {getPageTitle('Overview')} - - - - - {''} - - - {hasPermission(currentUser, 'CREATE_ROLES') && } - {!!rolesWidgets.length && - hasPermission(currentUser, 'CREATE_ROLES') && ( -

- {`${widgetsRole?.role?.label || 'Users'}'s widgets`} -

- )} -
- {(isFetchingQuery || loading) && ( -
- {' '} - Loading widgets... + const isInstructor = currentUser?.app_role?.name?.toLowerCase().includes('instructor') || currentUser?.app_role?.name?.toLowerCase().includes('admin'); + const isStudent = currentUser?.app_role?.name?.toLowerCase().includes('student'); + + return ( + <> + + {getPageTitle('Dashboard Overview')} + + + + {''} + + + {/* Role-Specific Hero Section */} +
+ +
+
+

+ {isInstructor ? 'Course Insights' : 'Your Learning Journey'} +

+

+ {isInstructor + ? 'Check how your courses are performing and engage with your students to boost completion rates.' + : 'You have completed 0% of your current goals. Jump back in and continue where you left off!'} +

+
+ +
+
+
+ +
+
+
+ + +

+ + Recent Activity +

+
+
+

Welcome to EduFlow!

+

Today, 10:00 AM

+
+
+

No recent course activity found.

+
+
+
- )} - { rolesWidgets && - rolesWidgets.map((widget) => ( - - ))} -
+ )} - {!!rolesWidgets.length &&
} - -
- - - {hasPermission(currentUser, 'READ_USERS') && -
-
-
-
- Users -
-
- {users} -
+
+ {(isFetchingQuery || loading) && ( +
+ + Loading widgets...
-
- -
-
+ )} + + {rolesWidgets && rolesWidgets.map((widget) => ( + + ))}
- } - - {hasPermission(currentUser, 'READ_ROLES') && -
-
-
-
- Roles -
-
- {roles} -
-
-
- -
-
+ + {!!rolesWidgets.length &&
} + +

Platform Statistics

+
+ {[ + { label: 'Courses', count: courses, href: '/courses/courses-list', icon: icon.mdiSchool, perm: 'READ_COURSES' }, + { label: 'Lessons', count: lessons, href: '/lessons/lessons-list', icon: icon.mdiPlayCircle, perm: 'READ_LESSONS' }, + { label: 'Enrollments', count: enrollments, href: '/enrollments/enrollments-list', icon: icon.mdiAccountMultiple, perm: 'READ_ENROLLMENTS' }, + { label: 'Students', count: users, href: '/users/users-list', icon: icon.mdiAccountGroup, perm: 'READ_USERS' }, + ].map((stat, idx) => ( + hasPermission(currentUser, stat.perm) && ( + +
+
+
+
{stat.label}
+
{stat.count}
+
+
+ +
+
+
+ + ) + ))}
- } - - {hasPermission(currentUser, 'READ_PERMISSIONS') && -
-
-
-
- Permissions -
-
- {permissions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_COURSES') && -
-
-
-
- Courses -
-
- {courses} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_LESSONS') && -
-
-
-
- Lessons -
-
- {lessons} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ENROLLMENTS') && -
-
-
-
- Enrollments -
-
- {enrollments} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PROGRESS') && -
-
-
-
- Progress -
-
- {progress} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_CATEGORIES') && -
-
-
-
- Categories -
-
- {categories} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_RESOURCES') && -
-
-
-
- Resources -
-
- {resources} -
-
-
- -
-
-
- } - - -
- - - ) + + + ) } Dashboard.getLayout = function getLayout(page: ReactElement) { - return {page} + return {page} } -export default Dashboard +export default Dashboard \ No newline at end of file diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 8f3dd24..cba7592 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,166 +1,234 @@ - -import React, { useEffect, useState } from 'react'; +import React, { useEffect } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; import Link from 'next/link'; import BaseButton from '../components/BaseButton'; -import CardBox from '../components/CardBox'; -import SectionFullScreen from '../components/SectionFullScreen'; import LayoutGuest from '../layouts/Guest'; -import BaseDivider from '../components/BaseDivider'; -import BaseButtons from '../components/BaseButtons'; import { getPageTitle } from '../config'; -import { useAppSelector } from '../stores/hooks'; -import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; -import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; - +import * as icon from '@mdi/js'; +import BaseIcon from '../components/BaseIcon'; +import { useAppDispatch, useAppSelector } from '../stores/hooks'; +import { fetch as fetchCourses } from '../stores/courses/coursesSlice'; export default function Starter() { - const [illustrationImage, setIllustrationImage] = useState({ - src: undefined, - photographer: undefined, - photographer_url: undefined, - }) - const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) - const [contentType, setContentType] = useState('video'); - const [contentPosition, setContentPosition] = useState('left'); - const textColor = useAppSelector((state) => state.style.linkColor); + const dispatch = useAppDispatch(); + const { courses, loading } = useAppSelector((state) => state.courses); - const title = 'Instructor-Student LMS' + useEffect(() => { + dispatch(fetchCourses({ query: '?published=true&limit=6' })); + }, [dispatch]); - // Fetch Pexels image/video - useEffect(() => { - async function fetchData() { - const image = await getPexelsImage(); - const video = await getPexelsVideo(); - setIllustrationImage(image); - setIllustrationVideo(video); - } - fetchData(); - }, []); + const title = 'EduFlow LMS'; - const imageBlock = (image) => ( - - ); + const features = [ + { + title: 'Expert-Led Courses', + description: 'Learn from industry professionals with real-world experience.', + icon: icon.mdiSchool, + }, + { + title: 'Flexible Learning', + description: 'Study at your own pace, anytime and anywhere in the world.', + icon: icon.mdiClockFast, + }, + { + title: 'Progress Tracking', + description: 'Monitor your growth with detailed analytics and assessments.', + icon: icon.mdiChartLine, + }, + ]; - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; + const getThumbnail = (course: any) => { + if (course.thumbnail && course.thumbnail.length > 0) { + return `/api/file/download?privateUrl=${course.thumbnail[0].privateUrl}`; + } + return 'https://images.pexels.com/photos/1181244/pexels-photo-1181244.jpeg?auto=compress&cs=tinysrgb&w=600'; + }; return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('Home')} - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - + {/* Hero Section */} +
+
+
+
+
+

+ Unlock Your Potential with EduFlow +

+

+ The most intuitive platform for instructors to create and students to excel. Start your journey today. +

+
- - - + +
+
+
+
+ Learning +
+
+
-
- -
-

© 2026 {title}. All rights reserved

- - Privacy Policy - -
+ + {/* Features Section */} +
+
+

Why Choose EduFlow?

+
+
+
+ {features.map((feature, idx) => ( +
+
+ +
+

{feature.title}

+

+ {feature.description} +

+
+ ))} +
+
+ + {/* Course Catalog Preview */} +
+
+
+
+

Popular Courses

+

Join thousands of students learning these top-rated skills.

+
+ +
+ + {loading ? ( +
+
+
+ ) : ( +
+ {courses && courses.length > 0 ? ( + courses.map((course: any, idx: number) => ( +
+
+ + {course.title} + + {course.category && ( +
+ + {course.category.name} + +
+ )} +
+
+
+ + {course.instructor ? `${course.instructor.firstName}${course.instructor.lastName ? " " + course.instructor.lastName : ""}` : 'Instructor'} +
+ +

+ {course.title} +

+ +
+
+ + {course.level} +
+
+ {course.price ? `$${course.price}` : 'Free'} +
+
+ +
+
+ )) + ) : ( +
+ No courses found. Check back later! +
+ )} +
+ )} +
+
+ + {/* Footer */} +
+
+
+

EduFlow.

+

+ Empowering the next generation of creators and learners through an accessible, robust, and beautiful LMS experience. +

+
+ + + +
+
+
+

Platform

+
    +
  • Browse Courses
  • +
  • Become Instructor
  • +
  • Mobile App
  • +
+
+
+

Support

+
    +
  • Privacy Policy
  • +
  • Terms of Service
  • +
  • Help Center
  • +
+
+
+
+

© 2026 EduFlow LMS. All rights reserved.

+
+ Generated with by Flatlogic +
+
+
); } Starter.getLayout = function getLayout(page: ReactElement) { return {page}; -}; - +}; \ No newline at end of file