Compare commits
No commits in common. "566f3bd5c1b0c1549682d336354d3f0be0da5081" and "d7bcff532f46c8ec99d1d4560a60a1a4b0091a72" have entirely different histories.
566f3bd5c1
...
d7bcff532f
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
@ -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",
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
@ -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 |
@ -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;
|
|
||||||
@ -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;
|
|
||||||
@ -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';
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch(fetch({}))
|
|
||||||
}, [dispatch])
|
|
||||||
|
|
||||||
if (loading) {
|
const perPage = 10
|
||||||
return <p>Loading...</p>
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const loadData = async (page = currentPage, request = filterRequest) => {
|
||||||
<table>
|
if (page !== currentPage) setCurrentPage(page);
|
||||||
<thead>
|
if (request !== filterRequest) setFilterRequest(request);
|
||||||
<tr>
|
const { sort, field } = sortModel[0];
|
||||||
<th>Title</th>
|
|
||||||
<th>Description</th>
|
const query = `?page=${page}&limit=${perPage}${request}&sort=${sort}&field=${field}`;
|
||||||
<th>Actions</th>
|
dispatch(fetch({ limit: perPage, page, query }));
|
||||||
</tr>
|
};
|
||||||
</thead>
|
|
||||||
<tbody>
|
useEffect(() => {
|
||||||
{courses &&
|
if (coursesNotify.showNotification) {
|
||||||
courses.map((course) => (
|
notify(coursesNotify.typeNotification, coursesNotify.textNotification);
|
||||||
<tr key={course.id}>
|
}
|
||||||
<td data-label="Title">{course.title}</td>
|
}, [coursesNotify.showNotification]);
|
||||||
<td data-label="Description">{course.description}</td>
|
|
||||||
<td className="actions-cell">
|
useEffect(() => {
|
||||||
<div className="buttons-right">
|
if (!currentUser) return;
|
||||||
<Link href={`/courses/${course.id}`}>
|
loadData();
|
||||||
<BaseButton color="info" icon={mdiPencil} small />
|
}, [sortModel, currentUser]);
|
||||||
</Link>
|
|
||||||
|
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>
|
</div>
|
||||||
</td>
|
)
|
||||||
</tr>
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{filterItems && Array.isArray( filterItems ) && filterItems.length ?
|
||||||
|
<CardBox>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
checkboxes: ['lorem'],
|
||||||
|
switches: ['lorem'],
|
||||||
|
radio: 'lorem',
|
||||||
|
}}
|
||||||
|
onSubmit={() => null}
|
||||||
|
>
|
||||||
|
<Form>
|
||||||
|
<>
|
||||||
|
{filterItems && filterItems.map((filterItem) => {
|
||||||
|
return (
|
||||||
|
<div key={filterItem.id} className="flex mb-4">
|
||||||
|
<div className="flex flex-col w-full mr-3">
|
||||||
|
<div className=" text-gray-500 font-bold">Filter</div>
|
||||||
|
<Field
|
||||||
|
className={controlClasses}
|
||||||
|
name='selectedField'
|
||||||
|
id='selectedField'
|
||||||
|
component='select'
|
||||||
|
value={filterItem?.fields?.selectedField || ''}
|
||||||
|
onChange={handleChange(filterItem.id)}
|
||||||
|
>
|
||||||
|
{filters.map((selectOption) => (
|
||||||
|
<option
|
||||||
|
key={selectOption.title}
|
||||||
|
value={`${selectOption.title}`}
|
||||||
|
>
|
||||||
|
{selectOption.label}
|
||||||
|
</option>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</Field>
|
||||||
</table>
|
</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
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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
|
||||||
@ -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
|
|
||||||
@ -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
|
|
||||||
@ -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'>
|
||||||
|
|||||||
@ -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';
|
||||||
|
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';
|
||||||
|
|
||||||
|
|
||||||
|
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('right');
|
||||||
|
const textColor = useAppSelector((state) => state.style.linkColor);
|
||||||
|
|
||||||
const title = 'CourseFlow LMS'
|
const title = 'CourseFlow LMS'
|
||||||
|
|
||||||
const features = [
|
// Fetch Pexels image/video
|
||||||
{
|
useEffect(() => {
|
||||||
icon: mdiBookOpenPageVariant,
|
async function fetchData() {
|
||||||
title: 'Interactive Courses',
|
const image = await getPexelsImage();
|
||||||
description: 'Engage your learners with rich multimedia content, quizzes, and assignments.',
|
const video = await getPexelsVideo();
|
||||||
},
|
setIllustrationImage(image);
|
||||||
{
|
setIllustrationVideo(video);
|
||||||
icon: mdiAccountGroup,
|
}
|
||||||
title: 'User Management',
|
fetchData();
|
||||||
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 = [
|
const imageBlock = (image) => (
|
||||||
{ href: 'https://github.com', icon: mdiGithub },
|
<div
|
||||||
{ href: 'https://twitter.com', icon: mdiTwitter },
|
className='hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3'
|
||||||
{ href: 'https://linkedin.com', icon: mdiLinkedin },
|
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 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>
|
</div>
|
||||||
</SectionFullScreen>
|
</SectionFullScreen>
|
||||||
|
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
|
||||||
<section className="py-20">
|
<p className='py-6 text-sm'>© 2024 <span>{title}</span>. All rights reserved</p>
|
||||||
<div className="container mx-auto px-6">
|
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||||
<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
|
Privacy Policy
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
);
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Landing.getLayout = function getLayout(page: ReactElement) {
|
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <LayoutGuest>{page}</LayoutGuest>
|
return <LayoutGuest>{page}</LayoutGuest>;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
@ -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;
|
|
||||||
Loading…
x
Reference in New Issue
Block a user