Compare commits

..

No commits in common. "566f3bd5c1b0c1549682d336354d3f0be0da5081" and "d7bcff532f46c8ec99d1d4560a60a1a4b0091a72" have entirely different histories.

21 changed files with 688 additions and 633 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@ -130,10 +130,6 @@ const CoursesData = [
"description": "Deep dive into hooks, context, performance optimization, and patterns.", "description": "Deep dive into hooks, context, performance optimization, and patterns.",
@ -141,21 +137,7 @@ const CoursesData = [
"status": "archived",
"status": "published",
@ -169,13 +151,6 @@ const CoursesData = [
"category": "Web Development", "category": "Web Development",
@ -183,13 +158,6 @@ const CoursesData = [
"price": 79.99, "price": 79.99,
@ -197,13 +165,6 @@ const CoursesData = [
"duration": 360, "duration": 360,
@ -211,13 +172,6 @@ const CoursesData = [
// type code here for "images" field // type code here for "images" field
@ -225,13 +179,6 @@ const CoursesData = [
"published_at": new Date('2025-12-05T09:00:00Z'), "published_at": new Date('2025-12-05T09:00:00Z'),
@ -239,35 +186,17 @@ const CoursesData = [
"enrollment_count": 64, "enrollment_count": 64,
}, },
{ {
"title": "UX for Developers", "title": "UX for Developers",
@ -275,13 +204,6 @@ const CoursesData = [
"description": "Design thinking and practical UX techniques for building better interfaces.", "description": "Design thinking and practical UX techniques for building better interfaces.",
@ -289,21 +211,7 @@ const CoursesData = [
"status": "archived",
"status": "published",
@ -316,6 +224,7 @@ const CoursesData = [
"category": "Design", "category": "Design",

View File

@ -15,7 +15,6 @@ const authRoutes = require('./routes/auth');
const fileRoutes = require('./routes/file'); const fileRoutes = require('./routes/file');
const searchRoutes = require('./routes/search'); const searchRoutes = require('./routes/search');
const pexelsRoutes = require('./routes/pexels'); const pexelsRoutes = require('./routes/pexels');
const publicRoutes = require('./routes/public');
const openaiRoutes = require('./routes/openai'); const openaiRoutes = require('./routes/openai');
@ -88,7 +87,6 @@ require('./auth/auth');
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use('/api/auth', authRoutes); app.use('/api/auth', authRoutes);
app.use('/api/public', publicRoutes);
app.use('/api/file', fileRoutes); app.use('/api/file', fileRoutes);
app.use('/api/pexels', pexelsRoutes); app.use('/api/pexels', pexelsRoutes);
app.enable('trust proxy'); app.enable('trust proxy');

View File

@ -1,17 +0,0 @@
const express = require('express');
const router = express.Router();
const CoursesDBApi = require('../db/api/courses');
router.get('/courses', async (req, res) => {
try {
const publishedCourses = await CoursesDBApi.findAll({
where: { status: 'published' },
});
res.json(publishedCourses);
} catch (error) {
console.error(error);
res.status(500).send('Internal Server Error');
}
});
module.exports = router;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@ -1,24 +0,0 @@
import React from 'react';
import ImageField from '../ImageField';
import Link from 'next/link';
type Props = {
course: any;
};
const PublicCourseCard = ({ course }: Props) => {
return (
<li className='glass-card-2'>
<Link href={`/courses/courses-view/?id=${course.id}`} className={'cursor-pointer'}>
<ImageField
image={course.cover}
className='w-full h-44 rounded-t-lg overflow-hidden'
imageClassName='h-full w-full object-cover'
/>
<p className={'px-6 py-4 font-semibold text-white'}>{course.title}</p>
</Link>
</li>
);
};
export default PublicCourseCard;

View File

@ -1,35 +0,0 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import PublicCourseCard from './PublicCourseCard';
import LoadingSpinner from '../LoadingSpinner';
const PublicCourseList = () => {
const [courses, setCourses] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
axios
.get('/public/courses')
.then((res) => {
setCourses(res.data.rows);
setLoading(false);
})
.catch((err) => {
console.error(err);
setLoading(false);
});
}, []);
return (
<div className='py-10'>
<h2 className='text-3xl font-bold text-center text-white mb-8'>Our Courses</h2>
{loading && <LoadingSpinner />}
<ul role='list' className='grid grid-cols-1 gap-x-6 gap-y-8 lg:grid-cols-3 2xl:grid-cols-4 xl:gap-x-8'>
{!loading &&
courses.map((course) => <PublicCourseCard key={course.id} course={course} />)}
</ul>
</div>
);
};
export default PublicCourseList;

View File

@ -1,52 +1,505 @@
import React, { useEffect } from 'react' import React, { useEffect, useState, useMemo } from 'react'
import { useAppDispatch, useAppSelector } from '../../stores/hooks' import { createPortal } from 'react-dom';
import { fetch } from '../../stores/courses/coursesSlice' import { ToastContainer, toast } from 'react-toastify';
import Link from 'next/link'
import { mdiPencil } from '@mdi/js'
import BaseButton from '../BaseButton' import BaseButton from '../BaseButton'
import CardBoxModal from '../CardBoxModal'
import CardBox from "../CardBox";
import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/courses/coursesSlice'
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Field, Form, Formik } from "formik";
import {
DataGrid,
GridColDef,
} from '@mui/x-data-grid';
import {loadColumns} from "./configureCoursesCols";
import _ from 'lodash';
import dataFormatter from '../../helpers/dataFormatter'
import {dataGridStyles} from "../../styles";
const TableCourses = () => {
const dispatch = useAppDispatch()
const router = useRouter()
const { courses, loading } = useAppSelector((state) => state.courses) import KanbanBoard from '../KanbanBoard/KanbanBoard';
import axios from 'axios';
const perPage = 10
const TableSampleCourses = ({ filterItems, setFilterItems, filters, showGrid }) => {
const notify = (type, msg) => toast( msg, {type, position: "bottom-center"});
const dispatch = useAppDispatch();
const router = useRouter();
const pagesList = [];
const [id, setId] = useState(null);
const [currentPage, setCurrentPage] = useState(0);
const [filterRequest, setFilterRequest] = React.useState('');
const [columns, setColumns] = useState<GridColDef[]>([]);
const [selectedRows, setSelectedRows] = useState([]);
const [sortModel, setSortModel] = useState([
{
field: '',
sort: 'desc',
},
]);
const [kanbanColumns, setKanbanColumns] = useState<Array<{id: string, label: string}> | null>(null);
const [kanbanFilters, setKanbanFilters] = useState('');
const { courses, loading, count, notify: coursesNotify, refetch } = useAppSelector((state) => state.courses)
const { currentUser } = useAppSelector((state) => state.auth);
const focusRing = useAppSelector((state) => state.style.focusRingColor);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
const corners = useAppSelector((state) => state.style.corners);
const numPages = Math.floor(count / perPage) === 0 ? 1 : Math.ceil(count / perPage);
for (let i = 0; i < numPages; i++) {
pagesList.push(i);
}
const loadData = async (page = currentPage, request = filterRequest) => {
if (page !== currentPage) setCurrentPage(page);
if (request !== filterRequest) setFilterRequest(request);
const { sort, field } = sortModel[0];
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
dispatch(fetch({ limit: perPage, page, query }));
};
useEffect(() => { useEffect(() => {
dispatch(fetch({})) if (coursesNotify.showNotification) {
}, [dispatch]) notify(coursesNotify.typeNotification, coursesNotify.textNotification);
}
}, [coursesNotify.showNotification]);
if (loading) { useEffect(() => {
return <p>Loading...</p> if (!currentUser) return;
loadData();
}, [sortModel, currentUser]);
useEffect(() => {
if (refetch) {
loadData(0);
dispatch(setRefetch(false));
}
}, [refetch, dispatch]);
const [isModalInfoActive, setIsModalInfoActive] = useState(false)
const [isModalTrashActive, setIsModalTrashActive] = useState(false)
const handleModalAction = () => {
setIsModalInfoActive(false)
setIsModalTrashActive(false)
} }
useEffect(() => {
setKanbanColumns([
{ id: "draft", label: "draft" },
{ id: "published", label: "published" },
{ id: "archived", label: "archived" },
]);
}, []);
const handleDeleteModalAction = (id: string) => {
setId(id)
setIsModalTrashActive(true)
}
const handleDeleteAction = async () => {
if (id) {
await dispatch(deleteItem(id));
await loadData(0);
setIsModalTrashActive(false);
}
};
const generateFilterRequests = useMemo(() => {
let request = '&';
filterItems.forEach((item) => {
const isRangeFilter = filters.find(
(filter) =>
filter.title === item.fields.selectedField &&
(filter.number || filter.date),
);
if (isRangeFilter) {
const from = item.fields.filterValueFrom;
const to = item.fields.filterValueTo;
if (from) {
request += `${item.fields.selectedField}Range=${from}&`;
}
if (to) {
request += `${item.fields.selectedField}Range=${to}&`;
}
} else {
const value = item.fields.filterValue;
if (value) {
request += `${item.fields.selectedField}=${value}&`;
}
}
});
return request;
}, [filterItems, filters]);
const deleteFilter = (value) => {
const newItems = filterItems.filter((item) => item.id !== value);
if (newItems.length) {
setFilterItems(newItems);
} else {
loadData(0, '');
setKanbanFilters('');
setFilterItems(newItems);
}
};
const handleSubmit = () => {
loadData(0, generateFilterRequests);
setKanbanFilters(generateFilterRequests);
};
const handleChange = (id) => (e) => {
const value = e.target.value;
const name = e.target.name;
setFilterItems(
filterItems.map((item) => {
if (item.id !== id) return item;
if (name === 'selectedField') return { id, fields: { [name]: value } };
return { id, fields: { ...item.fields, [name]: value } }
}),
);
};
const handleReset = () => {
setFilterItems([]);
loadData(0, '');
setKanbanFilters('');
};
const onPageChange = (page: number) => {
loadData(page);
setCurrentPage(page);
};
useEffect(() => {
if (!currentUser) return;
loadColumns(
handleDeleteModalAction,
`courses`,
currentUser,
).then((newCols) => setColumns(newCols));
}, [currentUser]);
const handleTableSubmit = async (id: string, data) => {
if (!_.isEmpty(data)) {
await dispatch(update({ id, data }))
.unwrap()
.then((res) => res)
.catch((err) => {
throw new Error(err);
});
}
};
const onDeleteRows = async (selectedRows) => {
await dispatch(deleteItemsByIds(selectedRows));
await loadData(0);
};
const controlClasses =
'w-full py-2 px-2 my-2 rounded dark:placeholder-gray-400 ' +
` ${bgColor} ${focusRing} ${corners} ` +
'dark:bg-slate-800 border';
const dataGrid = (
<div className='relative overflow-x-auto'>
<DataGrid
autoHeight
rowHeight={64}
sx={dataGridStyles}
className={'datagrid--table'}
getRowClassName={() => `datagrid--row`}
rows={courses ?? []}
columns={columns}
initialState={{
pagination: {
paginationModel: {
pageSize: 10,
},
},
}}
disableRowSelectionOnClick
onProcessRowUpdateError={(params) => {
console.log('Error', params);
}}
processRowUpdate={async (newRow, oldRow) => {
const data = dataFormatter.dataGridEditFormatter(newRow);
try {
await handleTableSubmit(newRow.id, data);
return newRow;
} catch {
return oldRow;
}
}}
sortingMode={'server'}
checkboxSelection
onRowSelectionModelChange={(ids) => {
setSelectedRows(ids)
}}
onSortModelChange={(params) => {
params.length
? setSortModel(params)
: setSortModel([{ field: '', sort: 'desc' }]);
}}
rowCount={count}
pageSizeOptions={[10]}
paginationMode={'server'}
loading={loading}
onPaginationModelChange={(params) => {
onPageChange(params.page);
}}
/>
</div>
)
return ( return (
<table> <>
<thead> {filterItems && Array.isArray( filterItems ) && filterItems.length ?
<tr> <CardBox>
<th>Title</th> <Formik
<th>Description</th> initialValues={{
<th>Actions</th> checkboxes: ['lorem'],
</tr> switches: ['lorem'],
</thead> radio: 'lorem',
<tbody> }}
{courses && onSubmit={() => null}
courses.map((course) => ( >
<tr key={course.id}> <Form>
<td data-label="Title">{course.title}</td> <>
<td data-label="Description">{course.description}</td> {filterItems && filterItems.map((filterItem) => {
<td className="actions-cell"> return (
<div className="buttons-right"> <div key={filterItem.id} className="flex mb-4">
<Link href={`/courses/${course.id}`}> <div className="flex flex-col w-full mr-3">
<BaseButton color="info" icon={mdiPencil} small /> <div className=" text-gray-500 font-bold">Filter</div>
</Link> <Field
</div> className={controlClasses}
</td> name='selectedField'
</tr> id='selectedField'
))} component='select'
</tbody> value={filterItem?.fields?.selectedField || ''}
</table> onChange={handleChange(filterItem.id)}
>
{filters.map((selectOption) => (
<option
key={selectOption.title}
value={`${selectOption.title}`}
>
{selectOption.label}
</option>
))}
</Field>
</div>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.type === 'enum' ? (
<div className="flex flex-col w-full mr-3">
<div className="text-gray-500 font-bold">
Value
</div>
<Field
className={controlClasses}
name="filterValue"
id='filterValue'
component="select"
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
>
<option value="">Select Value</option>
{filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.options?.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</Field>
</div>
) : filters.find((filter) =>
filter.title === filterItem?.fields?.selectedField
)?.number ? (
<div className="flex flex-row w-full mr-3">
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">From</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className="flex flex-col w-full">
<div className=" text-gray-500 font-bold">To</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
id='filterValueTo'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : filters.find(
(filter) =>
filter.title ===
filterItem?.fields?.selectedField
)?.date ? (
<div className='flex flex-row w-full mr-3'>
<div className='flex flex-col w-full mr-3'>
<div className=' text-gray-500 font-bold'>
From
</div>
<Field
className={controlClasses}
name='filterValueFrom'
placeholder='From'
id='filterValueFrom'
type='datetime-local'
value={filterItem?.fields?.filterValueFrom || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
<div className='flex flex-col w-full'>
<div className=' text-gray-500 font-bold'>To</div>
<Field
className={controlClasses}
name='filterValueTo'
placeholder='to'
id='filterValueTo'
type='datetime-local'
value={filterItem?.fields?.filterValueTo || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
</div>
) : (
<div className="flex flex-col w-full mr-3">
<div className=" text-gray-500 font-bold">Contains</div>
<Field
className={controlClasses}
name='filterValue'
placeholder='Contained'
id='filterValue'
value={filterItem?.fields?.filterValue || ''}
onChange={handleChange(filterItem.id)}
/>
</div>
)}
<div className="flex flex-col">
<div className=" text-gray-500 font-bold">Action</div>
<BaseButton
className="my-2"
type='reset'
color='danger'
label='Delete'
onClick={() => {
deleteFilter(filterItem.id)
}}
/>
</div>
</div>
)
})}
<div className="flex">
<BaseButton
className="my-2 mr-3"
color="success"
label='Apply'
onClick={handleSubmit}
/>
<BaseButton
className="my-2"
color='info'
label='Cancel'
onClick={handleReset}
/>
</div>
</>
</Form>
</Formik>
</CardBox> : null
}
<CardBoxModal
title="Please confirm"
buttonColor="info"
buttonLabel={loading ? 'Deleting...' : 'Confirm'}
isActive={isModalTrashActive}
onConfirm={handleDeleteAction}
onCancel={handleModalAction}
>
<p>Are you sure you want to delete this item?</p>
</CardBoxModal>
{!showGrid && kanbanColumns && (
<KanbanBoard
columnFieldName={'status'}
showFieldName={'title'}
entityName={'courses'}
filtersQuery={kanbanFilters}
deleteThunk={deleteItem}
updateThunk={update}
columns={kanbanColumns}
/>
)}
{showGrid && dataGrid}
{selectedRows.length > 0 &&
createPortal(
<BaseButton
className='me-4'
color='danger'
label={`Delete ${selectedRows.length === 1 ? 'Row' : 'Rows'}`}
onClick={() => onDeleteRows(selectedRows)}
/>,
document.getElementById('delete-rows-button'),
)}
<ToastContainer />
</>
) )
} }
export default TableCourses export default TableSampleCourses

View File

@ -1,40 +0,0 @@
.landing-gradient {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.glass-card {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.fade-in-up {
animation: fadeInUp 1s ease-out forwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

View File

@ -66,13 +66,8 @@ const menuAside: MenuAsideItem[] = [
}, },
{ {
href: '/profile', href: '/profile',
icon: icon.mdiAccountCircle,
label: 'Profile', label: 'Profile',
}, icon: icon.mdiAccountCircle,
{
href: '/courses',
label: 'Courses',
icon: icon.mdiBookOpenPageVariant,
}, },

View File

@ -6,7 +6,6 @@ import Head from 'next/head';
import { store } from '../stores/store'; import { store } from '../stores/store';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import '../css/main.css'; import '../css/main.css';
import '../css/_landing.css';
import axios from 'axios'; import axios from 'axios';
import { baseURLApi } from '../config'; import { baseURLApi } from '../config';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';

View File

@ -34,9 +34,9 @@ import ImageField from "../../components/ImageField";
const CoursesEdit = () => { const EditCourses = () => {
const router = useRouter() const router = useRouter()
const dispatch = useAppAppDispatch() const dispatch = useAppDispatch()
const initVals = { const initVals = {
@ -326,11 +326,11 @@ const CoursesEdit = () => {
const { courses } = useAppSelector((state) => state.courses) const { courses } = useAppSelector((state) => state.courses)
const { id } = router.query const { coursesId } = router.query
useEffect(() => { useEffect(() => {
dispatch(fetch({ id })) dispatch(fetch({ id: coursesId }))
}, [id]) }, [coursesId])
useEffect(() => { useEffect(() => {
if (typeof courses === 'object') { if (typeof courses === 'object') {
@ -350,8 +350,8 @@ const CoursesEdit = () => {
}, [courses]) }, [courses])
const handleSubmit = async (data) => { const handleSubmit = async (data) => {
await dispatch(update({ id, data })) await dispatch(update({ id: coursesId, data }))
await router.push('/courses') await router.push('/courses/courses-list')
} }
return ( return (
@ -786,7 +786,7 @@ const CoursesEdit = () => {
<BaseButtons> <BaseButtons>
<BaseButton type="submit" color="info" label="Submit" /> <BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" /> <BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/courses')}/> <BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/courses/courses-list')}/>
</BaseButtons> </BaseButtons>
</Form> </Form>
</Formik> </Formik>
@ -808,4 +808,4 @@ EditCourses.getLayout = function getLayout(page: ReactElement) {
) )
} }
export default CoursesEdit export default EditCourses

View File

@ -1,47 +0,0 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import React, { ReactElement } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import TableCourses from '../../components/Courses/TableCourses'
import BaseButton from '../../components/BaseButton'
import { useAppSelector } from '../../stores/hooks'
import { hasPermission } from '../../helpers/userPermissions'
const CoursesPage = () => {
const { currentUser } = useAppSelector((state) => state.auth)
const hasCreatePermission = currentUser && hasPermission(currentUser, 'CREATE_COURSES')
return (
<>
<Head>
<title>{getPageTitle('Courses')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Courses" main>
{''}
</SectionTitleLineWithButton>
{hasCreatePermission && (
<CardBox className="mb-6">
<BaseButton href={'/courses/new'} color="info" label="Create Course" />
</CardBox>
)}
<CardBox className="mb-6" hasTable>
<TableCourses />
</CardBox>
</SectionMain>
</>
)
}
CoursesPage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'READ_COURSES'}>{page}</LayoutAuthenticated>
}
export default CoursesPage

View File

@ -1,77 +0,0 @@
import { mdiChartTimelineVariant } from '@mdi/js'
import Head from 'next/head'
import React, { ReactElement } from 'react'
import CardBox from '../../components/CardBox'
import LayoutAuthenticated from '../../layouts/Authenticated'
import SectionMain from '../../components/SectionMain'
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'
import { getPageTitle } from '../../config'
import { Field, Form, Formik } from 'formik'
import FormField from '../../components/FormField'
import BaseDivider from '../../components/BaseDivider'
import BaseButtons from '../../components/BaseButtons'
import BaseButton from '../../components/BaseButton'
import { RichTextField } from '../../components/RichTextField'
import { create } from '../../stores/courses/coursesSlice'
import { useAppDispatch } from '../../stores/hooks'
import { useRouter } from 'next/router'
const initialValues = {
title: '',
description: '',
}
const CoursesNew = () => {
const router = useRouter()
const dispatch = useAppDispatch()
const handleSubmit = async (data) => {
await dispatch(create(data))
await router.push('/courses')
}
return (
<>
<Head>
<title>{getPageTitle('New Course')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Course" main>
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik initialValues={initialValues} onSubmit={(values) => handleSubmit(values)}>
<Form>
<FormField label="Title">
<Field name="title" placeholder="Course Title" />
</FormField>
<FormField label="Description">
<Field name="description" component={RichTextField} />
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton
type="reset"
color="danger"
outline
label="Cancel"
onClick={() => router.push('/courses')}
/>
</BaseButtons>
</Form>
</Formik>
</CardBox>
</SectionMain>
</>
)
}
CoursesNew.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated permission={'CREATE_COURSES'}>{page}</LayoutAuthenticated>
}
export default CoursesNew

View File

@ -98,6 +98,12 @@ const Dashboard = () => {
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
{hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser}
isFetchingQuery={isFetchingQuery}
setWidgetsRole={setWidgetsRole}
widgetsRole={widgetsRole}
/>}
{!!rolesWidgets.length && {!!rolesWidgets.length &&
hasPermission(currentUser, 'CREATE_ROLES') && ( hasPermission(currentUser, 'CREATE_ROLES') && (
<p className=' text-gray-500 dark:text-gray-400 mb-4'> <p className=' text-gray-500 dark:text-gray-400 mb-4'>

View File

@ -1,135 +1,173 @@
import React from 'react'
import type { ReactElement } from 'react'
import Head from 'next/head'
import Link from 'next/link'
import BaseButton from '../components/BaseButton'
import SectionFullScreen from '../components/SectionFullScreen'
import LayoutGuest from '../layouts/Guest'
import { getPageTitle } from '../config'
import { useAppSelector } from '../stores/hooks'
import {
mdiBookOpenPageVariant,
mdiAccountGroup,
mdiChartLine,
mdiGithub,
mdiTwitter,
mdiLinkedin,
} from '@mdi/js'
import BaseIcon from '../components/BaseIcon'
import PublicCourseList from '../components/Courses/PublicCourseList'
export default function Landing() { import React, { useEffect, useState } from 'react';
const textColor = useAppSelector((state) => state.style.linkColor) import type { ReactElement } from 'react';
const title = 'CourseFlow LMS' 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';
const features = [
{
icon: mdiBookOpenPageVariant,
title: 'Interactive Courses',
description: 'Engage your learners with rich multimedia content, quizzes, and assignments.',
},
{
icon: mdiAccountGroup,
title: 'User Management',
description: 'Easily manage users, roles, and permissions with our flexible system.',
},
{
icon: mdiChartLine,
title: 'Progress Tracking',
description: 'Monitor learner progress with detailed analytics and reporting tools.',
},
]
const socialLinks = [ export default function Starter() {
{ href: 'https://github.com', icon: mdiGithub }, const [illustrationImage, setIllustrationImage] = useState({
{ href: 'https://twitter.com', icon: mdiTwitter }, src: undefined,
{ href: 'https://linkedin.com', icon: mdiLinkedin }, photographer: undefined,
] photographer_url: undefined,
})
const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
const [contentType, setContentType] = useState('video');
const [contentPosition, setContentPosition] = useState('right');
const textColor = useAppSelector((state) => state.style.linkColor);
const title = 'CourseFlow LMS'
// Fetch Pexels image/video
useEffect(() => {
async function fetchData() {
const image = await getPexelsImage();
const video = await getPexelsVideo();
setIllustrationImage(image);
setIllustrationVideo(video);
}
fetchData();
}, []);
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 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>)
}
};
return ( return (
<div className="landing-gradient text-white"> <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',
}
: {}
}
>
<Head> <Head>
<title>{getPageTitle('Welcome')}</title> <title>{getPageTitle('Starter Page')}</title>
</Head> </Head>
<SectionFullScreen className="relative"> <SectionFullScreen bg='violet'>
<div className="container relative mx-auto px-4 text-center"> <div
<h1 className={`flex ${
className="text-4xl md:text-7xl font-bold mb-4 text-white opacity-0 fade-in-up" contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'
style={{ textShadow: '0 2px 8px rgba(0,0,0,0.5)', animationDelay: '0.2s' }} } min-h-screen w-full`}
> >
Welcome to {title} {contentType === 'image' && contentPosition !== 'background'
</h1> ? imageBlock(illustrationImage)
<p : null}
className="text-lg md:text-xl mb-8 text-white/90 opacity-0 fade-in-up" {contentType === 'video' && contentPosition !== 'background'
style={{ textShadow: '0 2px 8px rgba(0,0,0,0.5)', animationDelay: '0.4s' }} ? videoBlock(illustrationVideo)
> : null}
A modern and flexible platform for your online courses. <div className='flex items-center justify-center flex-col space-y-4 w-full lg:w-full'>
</p> <CardBox className='w-full md:w-3/5 lg:w-2/3'>
<div style={{ animationDelay: '0.6s' }} className="opacity-0 fade-in-up"> <CardBoxComponentTitle title="Welcome to your CourseFlow LMS app!"/>
<BaseButton href="/login" label="Get Started" color="info" size="lg" className="w-auto" />
</div> <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>
<BaseButton
href='/login'
label='Login'
color='info'
className='w-full'
/>
</BaseButtons>
<div className='grid grid-cols-1 gap-2 lg:grid-cols-4 mt-2'>
<div className='text-center'><a className={`${textColor}`} href='https://react.dev/'>React.js</a></div>
<div className='text-center'><a className={`${textColor}`} href='https://tailwindcss.com/'>Tailwind CSS</a></div>
<div className='text-center'><a className={`${textColor}`} href='https://nodejs.org/en'>Node.js</a></div>
<div className='text-center'><a className={`${textColor}`} href='https://flatlogic.com/forum'>Flatlogic Forum</a></div>
</div>
</CardBox>
</div> </div>
</div>
</SectionFullScreen> </SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2024 <span>{title}</span>. All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
</Link>
</div>
<section className="py-20">
<div className="container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold">Why Choose Us?</h2>
<p className="text-white/80 mt-2">
Everything you need to create and manage your online learning experience.
</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
{features.map((feature, index) => (
<div
key={index}
className="p-8 glass-card rounded-2xl text-center shadow-lg"
>
<BaseIcon path={feature.icon} size={48} className="mx-auto" />
<h3 className="text-xl font-semibold mt-4 mb-2">{feature.title}</h3>
<p className="text-white/90">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
<section className="py-20">
<div className="container mx-auto px-6">
<PublicCourseList />
</div>
</section>
<footer className="text-white py-10">
<div className="container mx-auto px-4 text-center">
<p className="text-sm text-white/70">
© {new Date().getFullYear()} <span>{title}</span>. All rights reserved.
</p>
<div className="flex justify-center space-x-4 mt-4">
{socialLinks.map((social, index) => (
<a
key={index}
href={social.href}
target="_blank"
rel="noopener noreferrer"
className="text-white/70 hover:text-white"
>
<BaseIcon path={social.icon} size={24} />
</a>
))}
</div>
<div className="mt-4">
<Link href="/privacy-policy" className={'text-sm text-white/70 hover:text-white'}>
Privacy Policy
</Link>
</div>
</div>
</footer>
</div> </div>
) );
} }
Landing.getLayout = function getLayout(page: ReactElement) { Starter.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest> return <LayoutGuest>{page}</LayoutGuest>;
} };

View File

@ -8,27 +8,27 @@ import { ToastContainer, toast } from 'react-toastify';
import DatePicker from 'react-datepicker'; import DatePicker from 'react-datepicker';
import 'react-datepicker/dist/react-datepicker.css'; import 'react-datepicker/dist/react-datepicker.css';
import CardBox from '../../components/CardBox'; import CardBox from '../components/CardBox';
import LayoutAuthenticated from '../../layouts/Authenticated'; import LayoutAuthenticated from '../layouts/Authenticated';
import SectionMain from '../../components/SectionMain'; import SectionMain from '../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton'; import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
import { getPageTitle } from '../../config'; import { getPageTitle } from '../config';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
import FormField from '../../components/FormField'; import FormField from '../components/FormField';
import BaseDivider from '../../components/BaseDivider'; import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../../components/BaseButtons'; import BaseButtons from '../components/BaseButtons';
import BaseButton from '../../components/BaseButton'; import BaseButton from '../components/BaseButton';
import FormCheckRadio from '../../components/FormCheckRadio'; import FormCheckRadio from '../components/FormCheckRadio';
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup'; import FormCheckRadioGroup from '../components/FormCheckRadioGroup';
import FormImagePicker from '../../components/FormImagePicker'; import FormImagePicker from '../components/FormImagePicker';
import { SwitchField } from '../../components/SwitchField'; import { SwitchField } from '../components/SwitchField';
import { SelectField } from '../../components/SelectField'; import { SelectField } from '../components/SelectField';
import { update, fetch } from '../../stores/users/usersSlice'; import { update, fetch } from '../stores/users/usersSlice';
import { useAppDispatch, useAppSelector } from '../../stores/hooks'; import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import {findMe} from "../../stores/authSlice"; import {findMe} from "../stores/authSlice";
const EditUsers = () => { const EditUsers = () => {
const { currentUser, isFetching, token } = useAppSelector( const { currentUser, isFetching, token } = useAppSelector(

View File

@ -1,103 +0,0 @@
import {
mdiChartTimelineVariant,
mdiPencil
} from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement } from 'react';
import CardBox from '../../components/CardBox';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
import { getPageTitle } from '../../config';
import BaseButton from '../../components/BaseButton';
import { useAppSelector } from '../../stores/hooks';
import { useRouter } from 'next/router';
import UserAvatar from '../../components/UserAvatar';
import LoadingSpinner from '../../components/LoadingSpinner';
const ProfilePage = () => {
const { currentUser } = useAppSelector(
(state) => state.auth,
);
const router = useRouter();
const handleEdit = () => {
router.push('/profile/edit');
}
if (!currentUser) {
return (
<SectionMain>
<LoadingSpinner />
</SectionMain>
);
}
return (
<>
<Head>
<title>{getPageTitle('Profile')}</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={mdiChartTimelineVariant}
title='Profile'
main
>
<BaseButton
icon={mdiPencil}
label='Edit'
color='info'
onClick={handleEdit}
/>
</SectionTitleLineWithButton>
<CardBox className="mb-6">
<div
className="h-48 bg-cover bg-center"
style={{
backgroundImage:
"url('https://images.pexels.com/photos/1761279/pexels-photo-1761279.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=1')",
}}
></div>
<div className="flex justify-center -mt-24">
<div className="w-32 h-32 border-4 border-white rounded-full overflow-hidden">
{currentUser.avatar && currentUser.avatar.length > 0 ? (
<img
className="w-full h-full object-cover"
src={currentUser.avatar[0].publicUrl}
alt="Avatar"
/>
) : (
<UserAvatar className="w-full h-full" />
)}
</div>
</div>
<div className="text-center mt-4">
<h1 className="text-2xl font-semibold">{`${currentUser?.firstName || ''} ${
currentUser?.lastName || ''
}`}</h1>
<p className="text-gray-500">{currentUser?.email}</p>
{currentUser.app_role && (
<p className="text-gray-500 mt-1 text-sm font-bold">{
currentUser.app_role.name.charAt(0).toUpperCase() +
currentUser.app_role.name.slice(1)
}</p>
)}
</div>
</CardBox>
</SectionMain>
</>
);
};
ProfilePage.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
};
export default ProfilePage;