From c41134fbf1874fe5c4e74c9dff9b3de4d4b69560 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 15 Jan 2026 16:17:15 +0000 Subject: [PATCH] v1 --- .../src/components/Courses/TableCourses.tsx | 531 ++---------------- frontend/src/menuAside.ts | 7 +- .../courses/{[coursesId].tsx => [id].tsx} | 18 +- frontend/src/pages/courses/index.tsx | 47 ++ frontend/src/pages/courses/new.tsx | 77 +++ frontend/src/pages/dashboard.tsx | 6 - frontend/src/pages/index.tsx | 204 ++----- .../pages/{profile.tsx => profile/edit.tsx} | 34 +- frontend/src/pages/profile/index.tsx | 103 ++++ 9 files changed, 341 insertions(+), 686 deletions(-) rename frontend/src/pages/courses/{[coursesId].tsx => [id].tsx} (95%) create mode 100644 frontend/src/pages/courses/index.tsx create mode 100644 frontend/src/pages/courses/new.tsx rename frontend/src/pages/{profile.tsx => profile/edit.tsx} (86%) create mode 100644 frontend/src/pages/profile/index.tsx diff --git a/frontend/src/components/Courses/TableCourses.tsx b/frontend/src/components/Courses/TableCourses.tsx index 27c67cc..66c51b3 100644 --- a/frontend/src/components/Courses/TableCourses.tsx +++ b/frontend/src/components/Courses/TableCourses.tsx @@ -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([]); - const [selectedRows, setSelectedRows] = useState([]); - const [sortModel, setSortModel] = useState([ - { - field: '', - sort: 'desc', - }, - ]); - - const [kanbanColumns, setKanbanColumns] = useState | 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

Loading...

} - - 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 = ( -
- `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); - }} - /> -
- ) - return ( - <> - {filterItems && Array.isArray( filterItems ) && filterItems.length ? - - null} - > -
- <> - {filterItems && filterItems.map((filterItem) => { - return ( -
-
-
Filter
- - {filters.map((selectOption) => ( - - ))} - -
- {filters.find((filter) => - filter.title === filterItem?.fields?.selectedField - )?.type === 'enum' ? ( -
-
- Value -
- - - {filters.find((filter) => - filter.title === filterItem?.fields?.selectedField - )?.options?.map((option) => ( - - ))} - -
- ) : filters.find((filter) => - filter.title === filterItem?.fields?.selectedField - )?.number ? ( -
-
-
From
- -
-
-
To
- -
-
- ) : filters.find( - (filter) => - filter.title === - filterItem?.fields?.selectedField - )?.date ? ( -
-
-
- From -
- -
-
-
To
- -
-
- ) : ( -
-
Contains
- -
- )} -
-
Action
- { - deleteFilter(filterItem.id) - }} - /> -
-
- ) - })} -
- - -
- -
-
-
: null - } - -

Are you sure you want to delete this item?

-
- - - - {!showGrid && kanbanColumns && ( - - )} - - - - {showGrid && dataGrid} - - - {selectedRows.length > 0 && - createPortal( - onDeleteRows(selectedRows)} - />, - document.getElementById('delete-rows-button'), - )} - - + + + + + + + + + + {courses && + courses.map((course) => ( + + + + + + ))} + +
TitleDescriptionActions
{course.title}{course.description} +
+ + + +
+
) } -export default TableSampleCourses +export default TableCourses \ No newline at end of file diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts index 1b4bc09..1722e4e 100644 --- a/frontend/src/menuAside.ts +++ b/frontend/src/menuAside.ts @@ -66,8 +66,13 @@ const menuAside: MenuAsideItem[] = [ }, { href: '/profile', - label: 'Profile', icon: icon.mdiAccountCircle, + label: 'Profile', + }, + { + href: '/courses', + label: 'Courses', + icon: icon.mdiBookOpenPageVariant, }, diff --git a/frontend/src/pages/courses/[coursesId].tsx b/frontend/src/pages/courses/[id].tsx similarity index 95% rename from frontend/src/pages/courses/[coursesId].tsx rename to frontend/src/pages/courses/[id].tsx index ca282ee..84441ea 100644 --- a/frontend/src/pages/courses/[coursesId].tsx +++ b/frontend/src/pages/courses/[id].tsx @@ -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 = () => { - router.push('/courses/courses-list')}/> + router.push('/courses')}/> @@ -808,4 +808,4 @@ EditCourses.getLayout = function getLayout(page: ReactElement) { ) } -export default EditCourses +export default CoursesEdit diff --git a/frontend/src/pages/courses/index.tsx b/frontend/src/pages/courses/index.tsx new file mode 100644 index 0000000..df38ed4 --- /dev/null +++ b/frontend/src/pages/courses/index.tsx @@ -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 ( + <> + + {getPageTitle('Courses')} + + + + {''} + + + {hasCreatePermission && ( + + + + )} + + + + + + + ) +} + +CoursesPage.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default CoursesPage diff --git a/frontend/src/pages/courses/new.tsx b/frontend/src/pages/courses/new.tsx new file mode 100644 index 0000000..6fcdf2a --- /dev/null +++ b/frontend/src/pages/courses/new.tsx @@ -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 ( + <> + + {getPageTitle('New Course')} + + + + {''} + + + handleSubmit(values)}> +
+ + + + + + + + + + + + + router.push('/courses')} + /> + + +
+
+
+ + ) +} + +CoursesNew.getLayout = function getLayout(page: ReactElement) { + return {page} +} + +export default CoursesNew diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index bc1e56c..c92c74e 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -98,12 +98,6 @@ const Dashboard = () => { {''} - {hasPermission(currentUser, 'CREATE_ROLES') && } {!!rolesWidgets.length && hasPermission(currentUser, 'CREATE_ROLES') && (

diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 4d79de8..eab193b 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -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) => ( -

- ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- - -
) - } - }; +export default function Landing() { + const textColor = useAppSelector((state) => state.style.linkColor) + const title = 'CourseFlow LMS' return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('Welcome')} - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - - -
+ +
+

+ Welcome to {title} +

+

+ A modern and flexible platform for your online courses. +

+
-
-
-

© 2024 {title}. All rights reserved

- - Privacy Policy - -
+
+
+

+ © {new Date().getFullYear()} {title}. All rights reserved. +

+ + Privacy Policy + +
+
- ); + ) } -Starter.getLayout = function getLayout(page: ReactElement) { - return {page}; -}; - +Landing.getLayout = function getLayout(page: ReactElement) { + return {page} +} \ No newline at end of file diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile/edit.tsx similarity index 86% rename from frontend/src/pages/profile.tsx rename to frontend/src/pages/profile/edit.tsx index f5eb7cf..25302dc 100644 --- a/frontend/src/pages/profile.tsx +++ b/frontend/src/pages/profile/edit.tsx @@ -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( diff --git a/frontend/src/pages/profile/index.tsx b/frontend/src/pages/profile/index.tsx new file mode 100644 index 0000000..b7e381f --- /dev/null +++ b/frontend/src/pages/profile/index.tsx @@ -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 ( + + + + ); + } + + return ( + <> + + {getPageTitle('Profile')} + + + + + + +
+
+
+ {currentUser.avatar && currentUser.avatar.length > 0 ? ( + Avatar + ) : ( + + )} +
+
+
+

{`${currentUser?.firstName || ''} ${ + currentUser?.lastName || '' + }`}

+

{currentUser?.email}

+ {currentUser.app_role && ( +

{ + currentUser.app_role.name.charAt(0).toUpperCase() + + currentUser.app_role.name.slice(1) + }

+ )} +
+
+
+ + ); +}; + +ProfilePage.getLayout = function getLayout(page: ReactElement) { + return {page}; +}; + +export default ProfilePage;