diff --git a/assets/pasted-20260115-162026-20208a7c.jpg b/assets/pasted-20260115-162026-20208a7c.jpg new file mode 100644 index 0000000..1caa96c Binary files /dev/null and b/assets/pasted-20260115-162026-20208a7c.jpg differ diff --git a/assets/pasted-20260115-162603-0edb92a7.jpg b/assets/pasted-20260115-162603-0edb92a7.jpg new file mode 100644 index 0000000..d86b8d4 Binary files /dev/null and b/assets/pasted-20260115-162603-0edb92a7.jpg differ diff --git a/backend/src/db/seeders/20231127130745-sample-data.js b/backend/src/db/seeders/20231127130745-sample-data.js index 5edbaf1..0e5a8db 100644 --- a/backend/src/db/seeders/20231127130745-sample-data.js +++ b/backend/src/db/seeders/20231127130745-sample-data.js @@ -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 diff --git a/backend/src/index.js b/backend/src/index.js index d0a4630..2d51171 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -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'); diff --git a/backend/src/routes/public.js b/backend/src/routes/public.js new file mode 100644 index 0000000..e256399 --- /dev/null +++ b/backend/src/routes/public.js @@ -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; \ No newline at end of file diff --git a/frontend/public/assets/vm-shot-2026-01-15T16-20-21-040Z.jpg b/frontend/public/assets/vm-shot-2026-01-15T16-20-21-040Z.jpg new file mode 100644 index 0000000..1caa96c Binary files /dev/null and b/frontend/public/assets/vm-shot-2026-01-15T16-20-21-040Z.jpg differ diff --git a/frontend/public/assets/vm-shot-2026-01-15T16-24-24-855Z.jpg b/frontend/public/assets/vm-shot-2026-01-15T16-24-24-855Z.jpg new file mode 100644 index 0000000..b3c4dfd Binary files /dev/null and b/frontend/public/assets/vm-shot-2026-01-15T16-24-24-855Z.jpg differ diff --git a/frontend/public/assets/vm-shot-2026-01-15T16-25-54-285Z.jpg b/frontend/public/assets/vm-shot-2026-01-15T16-25-54-285Z.jpg new file mode 100644 index 0000000..d86b8d4 Binary files /dev/null and b/frontend/public/assets/vm-shot-2026-01-15T16-25-54-285Z.jpg differ diff --git a/frontend/src/components/Courses/PublicCourseCard.tsx b/frontend/src/components/Courses/PublicCourseCard.tsx new file mode 100644 index 0000000..4a7c493 --- /dev/null +++ b/frontend/src/components/Courses/PublicCourseCard.tsx @@ -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 ( +
  • + + +

    {course.title}

    + +
  • + ); +}; + +export default PublicCourseCard; diff --git a/frontend/src/components/Courses/PublicCourseList.tsx b/frontend/src/components/Courses/PublicCourseList.tsx new file mode 100644 index 0000000..d3afec0 --- /dev/null +++ b/frontend/src/components/Courses/PublicCourseList.tsx @@ -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 ( +
    +

    Our Courses

    + {loading && } + +
    + ); +}; + +export default PublicCourseList; 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/css/_landing.css b/frontend/src/css/_landing.css new file mode 100644 index 0000000..93aff2b --- /dev/null +++ b/frontend/src/css/_landing.css @@ -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); + } +} 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/_app.tsx b/frontend/src/pages/_app.tsx index 02b0a0a..ddd9058 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -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'; 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..6ada819 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -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) => ( -

    - ); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
    - - -
    ) - } - }; + const socialLinks = [ + { href: 'https://github.com', icon: mdiGithub }, + { href: 'https://twitter.com', icon: mdiTwitter }, + { href: 'https://linkedin.com', icon: mdiLinkedin }, + ] 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 - -
    +
    +
    +
    +

    Why Choose Us?

    +

    + Everything you need to create and manage your online learning experience. +

    +
    +
    + {features.map((feature, index) => ( +
    + +

    {feature.title}

    +

    {feature.description}

    +
    + ))} +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +

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

    +
    + {socialLinks.map((social, index) => ( + + + + ))} +
    +
    + + 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;