v1
This commit is contained in:
parent
d7bcff532f
commit
c41134fbf1
@ -1,505 +1,52 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react'
|
||||
import { createPortal } from 'react-dom';
|
||||
import { ToastContainer, toast } from 'react-toastify';
|
||||
import BaseButton from '../BaseButton'
|
||||
import CardBoxModal from '../CardBoxModal'
|
||||
import CardBox from "../CardBox";
|
||||
import { fetch, update, deleteItem, setRefetch, deleteItemsByIds } from '../../stores/courses/coursesSlice'
|
||||
import React, { useEffect } from 'react'
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks'
|
||||
import { fetch } from '../../stores/courses/coursesSlice'
|
||||
import Link from 'next/link'
|
||||
import { mdiPencil } from '@mdi/js'
|
||||
import BaseButton from '../BaseButton'
|
||||
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()
|
||||
|
||||
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 }));
|
||||
};
|
||||
const { courses, loading } = useAppSelector((state) => state.courses)
|
||||
|
||||
useEffect(() => {
|
||||
if (coursesNotify.showNotification) {
|
||||
notify(coursesNotify.typeNotification, coursesNotify.textNotification);
|
||||
}
|
||||
}, [coursesNotify.showNotification]);
|
||||
dispatch(fetch({}))
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
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)
|
||||
if (loading) {
|
||||
return <p>Loading...</p>
|
||||
}
|
||||
|
||||
|
||||
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 (
|
||||
<>
|
||||
{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>
|
||||
))}
|
||||
</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 />
|
||||
</>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{courses &&
|
||||
courses.map((course) => (
|
||||
<tr key={course.id}>
|
||||
<td data-label="Title">{course.title}</td>
|
||||
<td data-label="Description">{course.description}</td>
|
||||
<td className="actions-cell">
|
||||
<div className="buttons-right">
|
||||
<Link href={`/courses/${course.id}`}>
|
||||
<BaseButton color="info" icon={mdiPencil} small />
|
||||
</Link>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
||||
export default TableSampleCourses
|
||||
export default TableCourses
|
||||
@ -66,8 +66,13 @@ const menuAside: MenuAsideItem[] = [
|
||||
},
|
||||
{
|
||||
href: '/profile',
|
||||
label: 'Profile',
|
||||
icon: icon.mdiAccountCircle,
|
||||
label: 'Profile',
|
||||
},
|
||||
{
|
||||
href: '/courses',
|
||||
label: 'Courses',
|
||||
icon: icon.mdiBookOpenPageVariant,
|
||||
},
|
||||
|
||||
|
||||
|
||||
@ -34,9 +34,9 @@ import ImageField from "../../components/ImageField";
|
||||
|
||||
|
||||
|
||||
const EditCourses = () => {
|
||||
const CoursesEdit = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const dispatch = useAppAppDispatch()
|
||||
const initVals = {
|
||||
|
||||
|
||||
@ -326,11 +326,11 @@ const EditCourses = () => {
|
||||
const { courses } = useAppSelector((state) => state.courses)
|
||||
|
||||
|
||||
const { coursesId } = router.query
|
||||
const { id } = router.query
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetch({ id: coursesId }))
|
||||
}, [coursesId])
|
||||
dispatch(fetch({ id }))
|
||||
}, [id])
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof courses === 'object') {
|
||||
@ -350,8 +350,8 @@ const EditCourses = () => {
|
||||
}, [courses])
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(update({ id: coursesId, data }))
|
||||
await router.push('/courses/courses-list')
|
||||
await dispatch(update({ id, data }))
|
||||
await router.push('/courses')
|
||||
}
|
||||
|
||||
return (
|
||||
@ -786,7 +786,7 @@ const EditCourses = () => {
|
||||
<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/courses-list')}/>
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/courses')}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
@ -808,4 +808,4 @@ EditCourses.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default EditCourses
|
||||
export default CoursesEdit
|
||||
47
frontend/src/pages/courses/index.tsx
Normal file
47
frontend/src/pages/courses/index.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
|
||||
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
|
||||
77
frontend/src/pages/courses/new.tsx
Normal file
77
frontend/src/pages/courses/new.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
|
||||
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,12 +98,6 @@ const Dashboard = () => {
|
||||
{''}
|
||||
</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'>
|
||||
|
||||
@ -1,173 +1,55 @@
|
||||
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 React, { useEffect, useState } 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';
|
||||
|
||||
|
||||
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'
|
||||
|
||||
// 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>)
|
||||
}
|
||||
};
|
||||
export default function Landing() {
|
||||
const textColor = useAppSelector((state) => state.style.linkColor)
|
||||
const title = 'CourseFlow LMS'
|
||||
|
||||
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="flex flex-col min-h-screen">
|
||||
<Head>
|
||||
<title>{getPageTitle('Starter Page')}</title>
|
||||
<title>{getPageTitle('Welcome')}</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 CourseFlow 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>
|
||||
<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>
|
||||
<SectionFullScreen bg="violet" className="flex-grow">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<h1 className="text-4xl md:text-6xl font-bold mb-4 text-white">
|
||||
Welcome to {title}
|
||||
</h1>
|
||||
<p className="text-lg md:text-xl mb-8 text-white/70">
|
||||
A modern and flexible platform for your online courses.
|
||||
</p>
|
||||
<BaseButton
|
||||
href="/login"
|
||||
label="Get Started"
|
||||
color="info"
|
||||
size="lg"
|
||||
className="w-auto"
|
||||
/>
|
||||
</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'>© 2024 <span>{title}</span>. All rights reserved</p>
|
||||
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<footer className="bg-gray-800 text-white py-6">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<p className="text-sm">
|
||||
© {new Date().getFullYear()} <span>{title}</span>. All rights reserved.
|
||||
</p>
|
||||
<Link href="/privacy-policy" className={`mt-2 inline-block text-sm ${textColor}`}>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
Starter.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>;
|
||||
};
|
||||
|
||||
Landing.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutGuest>{page}</LayoutGuest>
|
||||
}
|
||||
@ -8,27 +8,27 @@ import { ToastContainer, toast } from 'react-toastify';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
|
||||
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 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 FormCheckRadio from '../components/FormCheckRadio';
|
||||
import FormCheckRadioGroup from '../components/FormCheckRadioGroup';
|
||||
import FormImagePicker from '../components/FormImagePicker';
|
||||
import { SwitchField } from '../components/SwitchField';
|
||||
import { SelectField } from '../components/SelectField';
|
||||
import FormField from '../../components/FormField';
|
||||
import BaseDivider from '../../components/BaseDivider';
|
||||
import BaseButtons from '../../components/BaseButtons';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import FormCheckRadio from '../../components/FormCheckRadio';
|
||||
import FormCheckRadioGroup from '../../components/FormCheckRadioGroup';
|
||||
import FormImagePicker from '../../components/FormImagePicker';
|
||||
import { SwitchField } from '../../components/SwitchField';
|
||||
import { SelectField } from '../../components/SelectField';
|
||||
|
||||
import { update, fetch } from '../stores/users/usersSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../stores/hooks';
|
||||
import { update, fetch } from '../../stores/users/usersSlice';
|
||||
import { useAppDispatch, useAppSelector } from '../../stores/hooks';
|
||||
import { useRouter } from 'next/router';
|
||||
import {findMe} from "../stores/authSlice";
|
||||
import {findMe} from "../../stores/authSlice";
|
||||
|
||||
const EditUsers = () => {
|
||||
const { currentUser, isFetching, token } = useAppSelector(
|
||||
103
frontend/src/pages/profile/index.tsx
Normal file
103
frontend/src/pages/profile/index.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
|
||||
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