test
This commit is contained in:
parent
d517da7aee
commit
a29f901104
@ -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',
|
||||
}
|
||||
};
|
||||
};
|
||||
@ -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'));
|
||||
`);
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -26,7 +26,6 @@ trailingSlash: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
@ -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'
|
||||
|
||||
@ -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'
|
||||
|
||||
289
frontend/src/pages/course/[id].tsx
Normal file
289
frontend/src/pages/course/[id].tsx
Normal 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>;
|
||||
};
|
||||
@ -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
|
||||
@ -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>;
|
||||
};
|
||||
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user