This commit is contained in:
Flatlogic Bot 2026-01-15 16:17:15 +00:00
parent d7bcff532f
commit c41134fbf1
9 changed files with 341 additions and 686 deletions

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View 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;