This commit is contained in:
Flatlogic Bot 2026-01-21 16:24:37 +00:00
parent d517da7aee
commit a29f901104
9 changed files with 654 additions and 486 deletions

View File

@ -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',
}
};
};

View File

@ -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'));
`);
}
};

View File

@ -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);

View File

@ -26,7 +26,6 @@ trailingSlash: true,
},
],
},
}
export default nextConfig

View File

@ -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'

View File

@ -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'

View File

@ -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<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<any>(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 = () => (
<header className="bg-white border-b border-gray-100 sticky top-0 z-50">
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
<Link href="/" className="flex items-center space-x-2">
<span className="text-2xl font-bold text-indigo-600">{projectName}</span>
</Link>
<div className="flex items-center space-x-4">
<Link href="/" className="text-gray-600 hover:text-indigo-600 font-medium text-sm">
All Courses
</Link>
<BaseButton label="Login" color="info" small href="/login" />
</div>
</div>
</header>
);
const Footer = () => (
<footer className="bg-slate-900 text-slate-400 py-12 border-t border-slate-800">
<div className="container mx-auto px-4">
<div className="flex flex-col md:flex-row justify-between items-center">
<div className="mb-4 md:mb-0">
<span className="text-xl font-bold text-white">{projectName}</span>
<p className="text-sm mt-2">Empowering learners worldwide.</p>
</div>
<div className="text-sm">
© 2026 {projectName} LMS. All rights reserved.
</div>
</div>
</div>
</footer>
);
if (loading) {
return (
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow flex items-center justify-center bg-gray-50">
<div className="flex flex-col items-center">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500 mb-4"></div>
<p className="text-gray-500 animate-pulse">Loading course details...</p>
</div>
</main>
<Footer />
</div>
);
}
if (error || !course) {
return (
<div className="flex flex-col min-h-screen">
<Header />
<main className="flex-grow container mx-auto px-4 py-12 text-center bg-gray-50">
<div className="max-w-md mx-auto bg-white p-8 rounded-2xl shadow-sm border border-gray-100">
<h1 className="text-2xl font-bold text-gray-800 mb-4">Course not found</h1>
<p className="text-gray-600 mb-8">The course you are looking for might have been removed or the link is incorrect.</p>
<BaseButton label="Back to Home Catalog" color="info" onClick={() => router.push('/')} icon={mdiArrowLeft} />
</div>
</main>
<Footer />
</div>
);
}
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 (
<div className="flex flex-col min-h-screen bg-gray-50">
<Head>
<title>{`${course.title} | ${projectName}`}</title>
</Head>
<Header />
<main className="flex-grow">
{/* Hero Section */}
<div className="bg-indigo-900 text-white py-16">
<div className="container mx-auto px-4">
<div className="max-w-4xl">
<div className="flex items-center space-x-2 mb-4">
{course.category && (
<span className="bg-indigo-700 text-indigo-100 px-3 py-1 rounded-full text-sm font-semibold uppercase tracking-wide">
{course.category.name}
</span>
)}
<span className="bg-green-600 text-white px-3 py-1 rounded-full text-sm font-semibold uppercase tracking-wide">
{course.level || 'Beginner'}
</span>
</div>
<h1 className="text-4xl md:text-5xl font-extrabold mb-6 leading-tight">
{course.title}
</h1>
<p className="text-xl text-indigo-100 mb-8 max-w-3xl leading-relaxed">
{course.short_description}
</p>
<div className="flex flex-wrap gap-6 text-indigo-100">
<div className="flex items-center">
<BaseIcon path={mdiAccount} size={20} className="mr-2" />
<span>By <span className="font-semibold">{course.instructor ? `${course.instructor.firstName}${course.instructor.lastName ? " " + course.instructor.lastName : ""}` : 'Instructor'}</span></span>
</div>
{course.duration && (
<div className="flex items-center">
<BaseIcon path={mdiClockOutline} size={20} className="mr-2" />
<span>{course.duration} minutes</span>
</div>
)}
{course.language && (
<div className="flex items-center">
<BaseIcon path={mdiTranslate} size={20} className="mr-2" />
<span>{course.language}</span>
</div>
)}
</div>
</div>
</div>
</div>
<div className="container mx-auto px-4 py-12">
<div className="flex flex-col lg:flex-row gap-8">
{/* Main Content */}
<div className="lg:w-2/3">
<section className="bg-white rounded-2xl shadow-sm p-8 mb-8 border border-gray-100">
<h2 className="text-2xl font-bold text-gray-900 mb-6 border-b border-gray-50 pb-4">Course Description</h2>
<div className="prose max-w-none text-gray-700 leading-relaxed whitespace-pre-line">
{course.description || 'No description available.'}
</div>
</section>
<section className="bg-white rounded-2xl shadow-sm p-8 border border-gray-100">
<div className="flex items-center justify-between mb-6 border-b border-gray-50 pb-4">
<h2 className="text-2xl font-bold text-gray-900">Curriculum</h2>
<span className="text-indigo-600 font-bold bg-indigo-50 px-3 py-1 rounded-lg text-sm">
{course.lessons_course?.length || 0} lessons
</span>
</div>
<div className="space-y-4">
{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) => (
<div key={lesson.id} className="flex items-start p-4 rounded-xl border border-gray-100 hover:bg-indigo-50/30 transition-all group cursor-default">
<div className="flex-shrink-0 w-10 h-10 bg-indigo-50 text-indigo-600 rounded-full flex items-center justify-center font-bold mr-4 group-hover:bg-indigo-600 group-hover:text-white transition-colors">
{index + 1}
</div>
<div className="flex-grow">
<h3 className="font-bold text-gray-900 mb-1 group-hover:text-indigo-600 transition-colors">{lesson.title}</h3>
<p className="text-sm text-gray-500 line-clamp-2">{lesson.content || lesson.short_description || 'No content description.'}</p>
</div>
{lesson.duration && (
<div className="ml-4 text-xs font-medium text-gray-400 whitespace-nowrap pt-1">
{lesson.duration} min
</div>
)}
</div>
))
) : (
<div className="text-center py-12 bg-gray-50 rounded-xl border-2 border-dashed border-gray-200">
<BaseIcon path={mdiClockOutline} size={48} className="mx-auto text-gray-300 mb-4" />
<p className="text-gray-500 font-medium italic">
No lessons available for this course yet.
</p>
</div>
)}
</div>
</section>
</div>
{/* Sidebar */}
<div className="lg:w-1/3">
<div className="bg-white rounded-2xl shadow-xl p-6 sticky top-24 border border-gray-100 overflow-hidden">
<div className="mb-6 relative -mx-6 -mt-6">
<img
src={getThumbnail(course)}
alt={course.title}
className="w-full h-56 object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/20 to-transparent"></div>
</div>
<div className="mb-6">
<div className="flex items-baseline mb-4">
<span className="text-4xl font-black text-gray-900 mr-2">
{course.price === 0 || !course.price || course.price === '0' ? 'Free' : `$${course.price}`}
</span>
{course.price > 0 && (
<span className="text-gray-400 line-through text-lg">$349</span>
)}
</div>
<BaseButton
label="Enroll Now"
color="info"
className="w-full py-4 text-lg font-black rounded-xl shadow-lg shadow-indigo-200 hover:shadow-indigo-300 transition-all"
href="/login"
/>
<p className="text-center text-xs text-gray-500 mt-4 font-medium uppercase tracking-tighter">
30-Day Money-Back Guarantee
</p>
</div>
<div className="border-t border-gray-100 pt-6">
<h4 className="font-black text-gray-900 mb-4 uppercase text-xs tracking-widest">Course Features</h4>
<ul className="space-y-4">
<li className="flex items-center text-sm text-gray-700 font-medium">
<div className="w-8 h-8 rounded-lg bg-indigo-50 flex items-center justify-center mr-3">
<BaseIcon path={mdiClockOutline} size={18} className="text-indigo-600" />
</div>
{course.duration ? `${course.duration} minutes total` : 'Flexible duration'}
</li>
<li className="flex items-center text-sm text-gray-700 font-medium">
<div className="w-8 h-8 rounded-lg bg-green-50 flex items-center justify-center mr-3">
<BaseIcon path={mdiAccount} size={18} className="text-green-600" />
</div>
Direct instructor access
</li>
<li className="flex items-center text-sm text-gray-700 font-medium">
<div className="w-8 h-8 rounded-lg bg-amber-50 flex items-center justify-center mr-3">
<BaseIcon path={mdiTagOutline} size={18} className="text-amber-600" />
</div>
Certificate of completion
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</main>
<Footer />
</div>
);
}
CourseDetailsPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};

View File

@ -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 (
<>
<Head>
<title>
{getPageTitle('Overview')}
</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='Overview'
main>
{''}
</SectionTitleLineWithButton>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser}
isFetchingQuery={isFetchingQuery}
setWidgetsRole={setWidgetsRole}
widgetsRole={widgetsRole}
/>}
{!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
</p>
)}
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
{(isFetchingQuery || loading) && (
<div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
<BaseIcon
className={`${iconsColor} animate-spin mr-5`}
w='w-16'
h='h-16'
size={48}
path={icon.mdiLoading}
/>{' '}
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 (
<>
<Head>
<title>{getPageTitle('Dashboard Overview')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title={`Welcome back, ${currentUser?.firstName || 'User'}!`}
main
>
{''}
</SectionTitleLineWithButton>
{/* Role-Specific Hero Section */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<CardBox className="lg:col-span-2 bg-gradient-to-r from-indigo-500 to-purple-600 text-white border-none">
<div className="flex flex-col md:flex-row items-center justify-between">
<div className="mb-6 md:mb-0">
<h2 className="text-2xl font-bold mb-2">
{isInstructor ? 'Course Insights' : 'Your Learning Journey'}
</h2>
<p className="text-indigo-100 opacity-90 max-w-md">
{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!'}
</p>
<div className="mt-6">
<BaseButton
href={isInstructor ? '/courses/courses-new' : '/courses/courses-list'}
label={isInstructor ? 'Create New Course' : 'Continue Learning'}
color="white"
className="text-indigo-600 font-bold"
/>
</div>
</div>
<div className="hidden md:block">
<BaseIcon path={isInstructor ? icon.mdiSchool : icon.mdiBookOpenVariant} size={120} className="text-white opacity-20" />
</div>
</div>
</CardBox>
<CardBox>
<h3 className="text-lg font-bold mb-4 flex items-center">
<BaseIcon path={icon.mdiBellOutline} className="mr-2 text-indigo-500" />
Recent Activity
</h3>
<div className="space-y-4">
<div className="text-sm border-l-4 border-indigo-500 pl-3 py-1">
<p className="font-medium">Welcome to EduFlow!</p>
<p className="text-gray-500 text-xs">Today, 10:00 AM</p>
</div>
<div className="text-sm border-l-4 border-gray-300 pl-3 py-1">
<p className="font-medium text-gray-400 italic">No recent course activity found.</p>
</div>
</div>
</CardBox>
</div>
)}
{ rolesWidgets &&
rolesWidgets.map((widget) => (
<SmartWidget
key={widget.id}
userId={currentUser?.id}
widget={widget}
roleId={widgetsRole?.role?.value || ''}
admin={hasPermission(currentUser, 'CREATE_ROLES')}
{hasPermission(currentUser, 'CREATE_ROLES') && (
<WidgetCreator
currentUser={currentUser}
isFetchingQuery={isFetchingQuery}
setWidgetsRole={setWidgetsRole}
widgetsRole={widgetsRole}
/>
))}
</div>
)}
{!!rolesWidgets.length && <hr className='my-6 ' />}
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Users
</div>
<div className="text-3xl leading-tight font-semibold">
{users}
</div>
<div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
{(isFetchingQuery || loading) && (
<div className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
<BaseIcon className={`${iconsColor} animate-spin mr-5`} w='w-16' h='h-16' size={48} path={icon.mdiLoading} />
Loading widgets...
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiAccountGroup || icon.mdiTable}
/>
</div>
</div>
)}
{rolesWidgets && rolesWidgets.map((widget) => (
<SmartWidget
key={widget.id}
userId={currentUser?.id}
widget={widget}
roleId={widgetsRole?.role?.value || ''}
admin={hasPermission(currentUser, 'CREATE_ROLES')}
/>
))}
</div>
</Link>}
{hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Roles
</div>
<div className="text-3xl leading-tight font-semibold">
{roles}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
/>
</div>
</div>
{!!rolesWidgets.length && <hr className='my-6' />}
<h3 className="text-xl font-bold mb-6">Platform Statistics</h3>
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6'>
{[
{ 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) && (
<Link key={idx} href={stat.href}>
<div className={`${corners !== 'rounded-full' ? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6 hover:shadow-lg transition-shadow bg-white`}>
<div className="flex justify-between items-center">
<div>
<div className="text-sm uppercase tracking-wider text-gray-500 dark:text-gray-400 mb-1">{stat.label}</div>
<div className="text-3xl font-black">{stat.count}</div>
</div>
<div className={`p-3 rounded-xl bg-indigo-50 dark:bg-indigo-900/30 ${iconsColor}`}>
<BaseIcon path={stat.icon} size={32} />
</div>
</div>
</div>
</Link>
)
))}
</div>
</Link>}
{hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Permissions
</div>
<div className="text-3xl leading-tight font-semibold">
{permissions}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountOutline || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_COURSES') && <Link href={'/courses/courses-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Courses
</div>
<div className="text-3xl leading-tight font-semibold">
{courses}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiSchool' in icon ? icon['mdiSchool' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_LESSONS') && <Link href={'/lessons/lessons-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Lessons
</div>
<div className="text-3xl leading-tight font-semibold">
{lessons}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiPlayCircle' in icon ? icon['mdiPlayCircle' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_ENROLLMENTS') && <Link href={'/enrollments/enrollments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Enrollments
</div>
<div className="text-3xl leading-tight font-semibold">
{enrollments}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiAccountMultiple' in icon ? icon['mdiAccountMultiple' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PROGRESS') && <Link href={'/progress/progress-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Progress
</div>
<div className="text-3xl leading-tight font-semibold">
{progress}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiProgressCheck' in icon ? icon['mdiProgressCheck' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_CATEGORIES') && <Link href={'/categories/categories-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Categories
</div>
<div className="text-3xl leading-tight font-semibold">
{categories}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiShape' in icon ? icon['mdiShape' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_RESOURCES') && <Link href={'/resources/resources-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Resources
</div>
<div className="text-3xl leading-tight font-semibold">
{resources}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiFile' in icon ? icon['mdiFile' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
</div>
</SectionMain>
</>
)
</SectionMain>
</>
)
}
Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
export default Dashboard
export default Dashboard

View File

@ -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) => (
<div
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
style={{
backgroundImage: `${
image
? `url(${image?.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}}
>
<div className='flex justify-center w-full bg-blue-300/20'>
<a
className='text-[8px]'
href={image?.photographer_url}
target='_blank'
rel='noreferrer'
>
Photo by {image?.photographer} on Pexels
</a>
</div>
</div>
);
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 (
<div className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'>
<video
className='absolute top-0 left-0 w-full h-full object-cover'
autoPlay
loop
muted
>
<source src={video?.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
className='text-[8px]'
href={video?.user?.url}
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
</a>
</div>
</div>)
}
};
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 (
<div
style={
contentPosition === 'background'
? {
backgroundImage: `${
illustrationImage
? `url(${illustrationImage.src?.original})`
: 'linear-gradient(rgba(255, 255, 255, 0.5), rgba(255, 255, 255, 0.5))'
}`,
backgroundSize: 'cover',
backgroundPosition: 'left center',
backgroundRepeat: 'no-repeat',
}
: {}
}
>
<div className="bg-slate-50 dark:bg-slate-900 min-h-screen">
<Head>
<title>{getPageTitle('Starter Page')}</title>
<title>{getPageTitle('Home')}</title>
</Head>
<SectionFullScreen bg='violet'>
<div
className={`flex ${
contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
} min-h-screen w-full`}
>
{contentType === 'image' && contentPosition !== 'background'
? imageBlock(illustrationImage)
: null}
{contentType === 'video' && contentPosition !== 'background'
? videoBlock(illustrationVideo)
: null}
<div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
<CardBox className='w-full md:w-3/5 lg:w-2/3'>
<CardBoxComponentTitle title="Welcome to your Instructor-Student LMS app!"/>
<div className="space-y-3">
<p className='text-center text-gray-500'>This is a React.js/Node.js app generated by the <a className={`${textColor}`} href="https://flatlogic.com/generator">Flatlogic Web App Generator</a></p>
<p className='text-center text-gray-500'>For guides and documentation please check
your local README.md and the <a className={`${textColor}`} href="https://flatlogic.com/documentation">Flatlogic documentation</a></p>
</div>
<BaseButtons>
{/* Hero Section */}
<section className="relative overflow-hidden bg-indigo-600 text-white">
<div className="absolute inset-0 bg-[url('https://www.transparenttextures.com/patterns/cubes.png')] opacity-10"></div>
<div className="container mx-auto px-6 py-24 relative z-10">
<div className="flex flex-col lg:flex-row items-center">
<div className="lg:w-1/2 mb-12 lg:mb-0">
<h1 className="text-5xl lg:text-6xl font-extrabold leading-tight mb-6">
Unlock Your Potential with <span className="text-indigo-200">EduFlow</span>
</h1>
<p className="text-xl text-indigo-100 mb-10 max-w-lg">
The most intuitive platform for instructors to create and students to excel. Start your journey today.
</p>
<div className="flex flex-wrap gap-4">
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
href="/register"
label="Start Learning Now"
color="white"
className="px-8 py-3 font-bold rounded-full shadow-lg"
/>
</BaseButtons>
</CardBox>
<BaseButton
href="/login"
label="Instructor Portal"
color="info"
outline
className="px-8 py-3 font-bold rounded-full border-2 border-white text-white hover:bg-white hover:text-indigo-600"
/>
</div>
</div>
<div className="lg:w-1/2 relative">
<div className="bg-gradient-to-tr from-violet-500 to-indigo-500 rounded-2xl shadow-2xl p-4 transform rotate-2">
<img
src="https://images.pexels.com/photos/4050312/pexels-photo-4050312.jpeg?auto=compress&cs=tinysrgb&w=800"
alt="Learning"
className="rounded-xl shadow-inner -rotate-2"
/>
</div>
</div>
</div>
</div>
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
</section>
{/* Features Section */}
<section className="py-20 container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-3xl font-bold text-slate-800 dark:text-white mb-4">Why Choose EduFlow?</h2>
<div className="w-20 h-1 bg-indigo-500 mx-auto"></div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
{features.map((feature, idx) => (
<div key={idx} className="bg-white dark:bg-slate-800 p-8 rounded-2xl shadow-sm hover:shadow-xl transition-shadow border border-slate-100 dark:border-slate-700 text-center">
<div className="inline-flex items-center justify-center w-16 h-16 bg-indigo-100 dark:bg-indigo-900 text-indigo-600 dark:text-indigo-300 rounded-full mb-6">
<BaseIcon path={feature.icon} size={32} />
</div>
<h3 className="text-xl font-bold mb-4 text-slate-800 dark:text-white">{feature.title}</h3>
<p className="text-slate-600 dark:text-slate-400 leading-relaxed">
{feature.description}
</p>
</div>
))}
</div>
</section>
{/* Course Catalog Preview */}
<section className="py-20 bg-slate-100 dark:bg-slate-800/50">
<div className="container mx-auto px-6">
<div className="flex justify-between items-end mb-12">
<div>
<h2 className="text-3xl font-bold text-slate-800 dark:text-white mb-2">Popular Courses</h2>
<p className="text-slate-600 dark:text-slate-400">Join thousands of students learning these top-rated skills.</p>
</div>
<BaseButton href="/login" label="Browse All" color="info" outline />
</div>
{loading ? (
<div className="flex justify-center items-center py-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600"></div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{courses && courses.length > 0 ? (
courses.map((course: any, idx: number) => (
<div key={idx} className="bg-white dark:bg-slate-800 rounded-2xl overflow-hidden shadow-md hover:shadow-2xl transition-all group border border-slate-200 dark:border-slate-700">
<div className="h-48 relative overflow-hidden">
<Link href={`/course/${course.id}`}>
<img
src={getThumbnail(course)}
alt={course.title}
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500 cursor-pointer"
/>
</Link>
{course.category && (
<div className="absolute top-4 left-4">
<span className="bg-indigo-600 text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wider">
{course.category.name}
</span>
</div>
)}
</div>
<div className="p-6">
<div className="flex items-center text-xs text-slate-500 mb-2">
<BaseIcon path={icon.mdiAccountCircle} size={14} className="mr-1" />
<span>{course.instructor ? `${course.instructor.firstName}${course.instructor.lastName ? " " + course.instructor.lastName : ""}` : 'Instructor'}</span>
</div>
<Link href={`/course/${course.id}`}>
<h3 className="text-xl font-bold mb-4 text-slate-800 dark:text-white group-hover:text-indigo-600 transition-colors cursor-pointer">
{course.title}
</h3>
</Link>
<div className="flex items-center justify-between">
<div className="flex items-center text-sm font-medium text-slate-700 dark:text-slate-300">
<BaseIcon path={icon.mdiSchool} size={16} className="mr-1 text-indigo-500" />
<span>{course.level}</span>
</div>
<div className="text-xl font-black text-indigo-600">
{course.price ? `$${course.price}` : 'Free'}
</div>
</div>
<BaseButton
href={`/course/${course.id}`}
label="View Details"
color="info"
className="w-full mt-6 py-3 rounded-xl"
/>
</div>
</div>
))
) : (
<div className="col-span-full text-center py-12 text-slate-500">
No courses found. Check back later!
</div>
)}
</div>
)}
</div>
</section>
{/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 px-6 border-t border-slate-800">
<div className="container mx-auto grid grid-cols-1 md:grid-cols-4 gap-12">
<div className="col-span-1 md:col-span-2">
<h2 className="text-2xl font-bold text-white mb-6">EduFlow<span className="text-indigo-500">.</span></h2>
<p className="max-w-sm mb-6 leading-relaxed">
Empowering the next generation of creators and learners through an accessible, robust, and beautiful LMS experience.
</p>
<div className="flex space-x-4">
<BaseIcon path={icon.mdiTwitter} size={20} className="hover:text-white cursor-pointer" />
<BaseIcon path={icon.mdiFacebook} size={20} className="hover:text-white cursor-pointer" />
<BaseIcon path={icon.mdiLinkedin} size={20} className="hover:text-white cursor-pointer" />
</div>
</div>
<div>
<h4 className="text-white font-bold mb-6 uppercase text-sm tracking-widest">Platform</h4>
<ul className="space-y-4">
<li><Link href="/login" className="hover:text-white">Browse Courses</Link></li>
<li><Link href="/register" className="hover:text-white">Become Instructor</Link></li>
<li><Link href="/login" className="hover:text-white">Mobile App</Link></li>
</ul>
</div>
<div>
<h4 className="text-white font-bold mb-6 uppercase text-sm tracking-widest">Support</h4>
<ul className="space-y-4">
<li><Link href="/privacy-policy" className="hover:text-white">Privacy Policy</Link></li>
<li><Link href="/terms-of-use" className="hover:text-white">Terms of Service</Link></li>
<li><Link href="#" className="hover:text-white">Help Center</Link></li>
</ul>
</div>
</div>
<div className="container mx-auto mt-12 pt-8 border-t border-slate-800 text-sm text-center md:text-left flex flex-col md:flex-row justify-between items-center">
<p>© 2026 EduFlow LMS. All rights reserved.</p>
<div className="mt-4 md:mt-0">
Generated with <span className="text-pink-500"></span> by Flatlogic
</div>
</div>
</footer>
</div>
);
}
Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
};