Forced merge: merge ai-dev into master

This commit is contained in:
Flatlogic Bot 2026-01-15 16:40:00 +00:00
commit 566f3bd5c1
21 changed files with 633 additions and 688 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

@ -129,97 +129,188 @@ const CoursesData = [
"description": "Deep dive into hooks, context, performance optimization, and patterns.",
"status": "archived",
"status": "published",
// type code here for "relation_one" field
"category": "Web Development",
"price": 79.99,
"duration": 360,
// type code here for "images" field
"published_at": new Date('2025-12-05T09:00:00Z'),
"enrollment_count": 64,
},
{
"title": "UX for Developers",
"description": "Design thinking and practical UX techniques for building better interfaces.",
"status": "archived",
"status": "published",
// type code here for "relation_one" field
// type code here for "relation_one" field

View File

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

View File

@ -0,0 +1,17 @@
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.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

View File

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

View File

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

View File

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

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

View File

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

View File

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

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,135 @@
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'
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 Landing() {
const textColor = useAppSelector((state) => state.style.linkColor)
const title = 'CourseFlow LMS'
const features = [
{
icon: mdiBookOpenPageVariant,
title: 'Interactive Courses',
description: 'Engage your learners with rich multimedia content, quizzes, and assignments.',
},
{
icon: mdiAccountGroup,
title: 'User Management',
description: 'Easily manage users, roles, and permissions with our flexible system.',
},
{
icon: mdiChartLine,
title: 'Progress Tracking',
description: 'Monitor learner progress with detailed analytics and reporting tools.',
},
]
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>)
}
};
const socialLinks = [
{ href: 'https://github.com', icon: mdiGithub },
{ href: 'https://twitter.com', icon: mdiTwitter },
{ href: 'https://linkedin.com', icon: mdiLinkedin },
]
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="landing-gradient text-white">
<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 className="relative">
<div className="container relative mx-auto px-4 text-center">
<h1
className="text-4xl md:text-7xl font-bold mb-4 text-white opacity-0 fade-in-up"
style={{ textShadow: '0 2px 8px rgba(0,0,0,0.5)', animationDelay: '0.2s' }}
>
Welcome to {title}
</h1>
<p
className="text-lg md:text-xl mb-8 text-white/90 opacity-0 fade-in-up"
style={{ textShadow: '0 2px 8px rgba(0,0,0,0.5)', animationDelay: '0.4s' }}
>
A modern and flexible platform for your online courses.
</p>
<div style={{ animationDelay: '0.6s' }} className="opacity-0 fade-in-up">
<BaseButton href="/login" label="Get Started" color="info" size="lg" className="w-auto" />
</div>
</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>
<section className="py-20">
<div className="container mx-auto px-6">
<div className="text-center mb-16">
<h2 className="text-4xl font-bold">Why Choose Us?</h2>
<p className="text-white/80 mt-2">
Everything you need to create and manage your online learning experience.
</p>
</div>
<div className="grid md:grid-cols-3 gap-8">
{features.map((feature, index) => (
<div
key={index}
className="p-8 glass-card rounded-2xl text-center shadow-lg"
>
<BaseIcon path={feature.icon} size={48} className="mx-auto" />
<h3 className="text-xl font-semibold mt-4 mb-2">{feature.title}</h3>
<p className="text-white/90">{feature.description}</p>
</div>
))}
</div>
</div>
</section>
<section className="py-20">
<div className="container mx-auto px-6">
<PublicCourseList />
</div>
</section>
<footer className="text-white py-10">
<div className="container mx-auto px-4 text-center">
<p className="text-sm text-white/70">
© {new Date().getFullYear()} <span>{title}</span>. All rights reserved.
</p>
<div className="flex justify-center space-x-4 mt-4">
{socialLinks.map((social, index) => (
<a
key={index}
href={social.href}
target="_blank"
rel="noopener noreferrer"
className="text-white/70 hover:text-white"
>
<BaseIcon path={social.icon} size={24} />
</a>
))}
</div>
<div className="mt-4">
<Link href="/privacy-policy" className={'text-sm text-white/70 hover:text-white'}>
Privacy Policy
</Link>
</div>
</div>
</footer>
</div>
);
)
}
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;