diff --git a/src/core/components/AppRoutes/index.tsx b/src/core/components/AppRoutes/index.tsx index 74e2c8e..54905c8 100644 --- a/src/core/components/AppRoutes/index.tsx +++ b/src/core/components/AppRoutes/index.tsx @@ -11,6 +11,8 @@ import Settings from "../../../features/Settings"; import PageNotFound from "../../../features/PageNotFound"; import LessonPage from "../../../features/Lessons/LessonPage"; import LessonPageEditor from "../../../features/Lessons/LessonPageEditor"; +import TeamCreateUser from "features/Team/CreateUser"; +import AccountSettings from "features/AccountSettings"; export default function AppRoutes() { return ( @@ -51,6 +53,15 @@ export default function AppRoutes() { } /> + + + + } + /> + - @@ -69,6 +80,15 @@ export default function AppRoutes() { } /> + + + + } + /> + {children} + return {children} ; } diff --git a/src/core/components/SideMenu/index.tsx b/src/core/components/SideMenu/index.tsx index e285d27..ee96031 100644 --- a/src/core/components/SideMenu/index.tsx +++ b/src/core/components/SideMenu/index.tsx @@ -7,9 +7,9 @@ import { TeamOutlined, WalletOutlined, } from "@ant-design/icons"; -import { Divider, Flex, Menu, Typography } from "antd"; +import { Divider, Flex, Form, Menu, Select, Typography } from "antd"; import { useEffect, useState } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; import { setIsSideMenuCollapsed, @@ -20,16 +20,21 @@ import { ItemType, MenuItemType } from "antd/es/menu/interface"; import { BreakpointLgWidth, Constants } from "core/utils/utils"; import Search from "antd/es/input/Search"; import { MyContainer } from "shared/components/MyContainer"; -import { addLessonContent } from "features/Lessons/LessonPageEditor/lessonPageEditorSlice"; - +import { + addLessonContent, + currentLessonId, + lessonState, +} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice"; import { Component, componentsGroups } from "features/Lessons/components"; - import { darkMode } from "core/reducers/appSlice"; - import { DndContext, DragOverlay, useDraggable } from "@dnd-kit/core"; -import { CSS } from "@dnd-kit/utilities"; -import { snapCenterToCursor } from "@dnd-kit/modifiers"; import { createPortal } from "react-dom"; +import { LessonState } from "core/types/lesson"; +import { useForm } from "antd/es/form/Form"; +import { + useAddLessonContentMutation, + useUpdateLessonStateMutation, +} from "core/services/lessons"; export function SideMenuContent() { const location = useLocation(); @@ -211,60 +216,105 @@ export function SideMenuContent() { export function SideMenuEditorContent() { // create is dragging useState const [isDragging, setIsDragging] = useState(null); + const { lessonId } = useParams(); + + const [form] = useForm(); + + const lnState = useSelector(lessonState); + const currentLnId = useSelector(currentLessonId); + + const [updateLessonState] = useUpdateLessonStateMutation(); + + console.log("lesson state", lnState); + + useEffect(() => { + form.setFieldsValue({ + state: lnState, + }); + }, [lnState]); return ( - - + + + - { - console.log("drag start", event.active.id); + { + console.log("drag start", event.active.id); - setIsDragging(event.active.id.toString()); - }} - onDragEnd={(evnet) => { - console.log("drag end", evnet.active.id); - setIsDragging(null); - }} - > - {componentsGroups.map((group, i) => ( -
- {group.category} + setIsDragging(event.active.id.toString()); + }} + onDragEnd={(evnet) => { + console.log("drag end", evnet.active.id); + setIsDragging(null); + }} + > + {componentsGroups.map((group, i) => ( +
+ {group.category} - - {group.components.map((component, i) => ( - - ))} - -
- ))} - {createPortal( - - {isDragging - ? (() => { - const comp = componentsGroups - .flatMap((group) => group.components) - .find((comp) => "draggable_" + comp.name === isDragging); - console.log("dragging", comp); - if (!comp) { - return null; - } + + {group.components.map((component, i) => ( + + ))} + +
+ ))} + {createPortal( + + {isDragging + ? (() => { + const comp = componentsGroups + .flatMap((group) => group.components) + .find((comp) => "draggable_" + comp.name === isDragging); + console.log("dragging", comp); + if (!comp) { + return null; + } - return ( -
- -
- ); - })() - : null} -
, - document.body - )} -
-
+ return ( +
+ +
+ ); + })() + : null} + , + document.body + )} + +
+ +
+
+ + + +
+
+ ); } @@ -275,27 +325,24 @@ export function DraggableCreateComponent({ }) { const dispatch = useDispatch(); - const { attributes, listeners, setNodeRef, transform, isDragging, active } = + /*const { attributes, listeners, setNodeRef, transform, isDragging, active } = useDraggable({ id: "draggable_" + component.name, - }); + });*/ return ( <>
{ - console.log("insert component", component.type); - dispatch(addLessonContent(component.type)); - }} + //ref={setNodeRef} + //{...attributes} + //{...listeners} + style={ + { + //transition: !isDragging ? "all 0.3s ease-out" : "none", + //boxShadow: isDragging ? "0px 0px 10px 0px #00000040" : "none", + //opacity: isDragging ? 0.25 : 1, + } + } >
@@ -307,6 +354,10 @@ function CreateComponent({ component }: { component: Component }) { const dispatch = useDispatch(); const isDarkMode = useSelector(darkMode); + const currentLnId = useSelector(currentLessonId); + + const [reqAddLessonContent] = useAddLessonContentMutation(); + return ( { + onClick={async () => { console.log("insert component", component.type); - dispatch(addLessonContent(component.type)); + + try { + const res = await reqAddLessonContent({ + lessonId: currentLnId, + type: component.type, + data: component.defaultData || "", + }).unwrap(); + + console.log("add content", component); + + dispatch( + addLessonContent({ + Id: res.Id, + Type: component.type, + Data: component.defaultData || "", + Page: 1, + Position: 1, + }) + ); + } catch (err) { + console.log("error", err); + } }} > {component.thumbnail ? ( diff --git a/src/core/services/lessons.ts b/src/core/services/lessons.ts index 631f860..260c8fb 100644 --- a/src/core/services/lessons.ts +++ b/src/core/services/lessons.ts @@ -2,10 +2,10 @@ import { createApi } from "@reduxjs/toolkit/query/react"; import { baseQueryWithErrorHandling } from "core/helper/api"; import { Lesson, - LessonPreview, + LessonContent, + LessonSettings, UpdateLessonPreviewThumbnail, } from "core/types/lesson"; -import { LessonContent } from "features/Lessons/LessonPage"; export const lessonsApi = createApi({ reducerPath: "lessonsApi", @@ -23,9 +23,9 @@ export const lessonsApi = createApi({ method: "POST", }), }), - getLessonPreview: builder.query({ + getLessonSettings: builder.query({ query: (lessonId) => ({ - url: `lessons/${lessonId}/preview`, + url: `lessons/${lessonId}/settings`, method: "GET", }), }), @@ -52,14 +52,60 @@ export const lessonsApi = createApi({ body: formData, }), }), + updateLessonState: builder.mutation({ + query: ({ lessonId, newState }) => ({ + url: `lessons/${lessonId}/state`, + method: "PATCH", + body: { State: newState }, + }), + }), + addLessonContent: builder.mutation({ + query: ({ lessonId, type, data }) => ({ + url: `lessons/${lessonId}/contents`, + method: "POST", + body: { + Type: type, + Data: data, + }, + }), + }), + updateLessonContent: builder.mutation({ + query: ({ lessonId, contentId, data }) => ({ + url: `lessons/${lessonId}/contents/${contentId}`, + method: "PATCH", + body: { + Data: data, + }, + }), + }), + updateLessonContentPosition: builder.mutation({ + query: ({ lessonId, contentId, newPosition }) => ({ + url: `lessons/${lessonId}/contents/${contentId}/position`, + method: "PATCH", + body: { + Position: newPosition, + }, + }), + }), + deleteLessonContent: builder.mutation({ + query: ({ lessonId, contentId }) => ({ + url: `lessons/${lessonId}/contents/${contentId}`, + method: "DELETE", + }), + }), }), }); export const { useGetLessonsQuery, useCreateLessonMutation, - useGetLessonPreviewQuery, + useGetLessonSettingsQuery, useGetLessonContentsQuery, useUpdateLessonPreviewTitleMutation, useUpdateLessonPreviewThumbnailMutation, + useUpdateLessonStateMutation, + useAddLessonContentMutation, + useUpdateLessonContentMutation, + useUpdateLessonContentPositionMutation, + useDeleteLessonContentMutation, } = lessonsApi; diff --git a/src/core/services/team.ts b/src/core/services/team.ts new file mode 100644 index 0000000..d2c8d7a --- /dev/null +++ b/src/core/services/team.ts @@ -0,0 +1,18 @@ +import { createApi } from "@reduxjs/toolkit/query/react"; +import { baseQueryWithErrorHandling } from "core/helper/api"; +import { TeamMember } from "core/types/team"; + +export const teamApi = createApi({ + reducerPath: "teamApi", + baseQuery: baseQueryWithErrorHandling, + endpoints: (builder) => ({ + getTeam: builder.query({ + query: () => ({ + url: "team/members", + method: "GET", + }), + }), + }), +}); + +export const { useGetTeamQuery } = teamApi; diff --git a/src/core/store/store.tsx b/src/core/store/store.tsx index b246658..dae19eb 100644 --- a/src/core/store/store.tsx +++ b/src/core/store/store.tsx @@ -4,6 +4,7 @@ import { sideMenuSlice } from "../components/SideMenu/sideMenuSlice"; import { lessonPageEditorSlice } from "../../features/Lessons/LessonPageEditor/lessonPageEditorSlice"; import { appSlice } from "../reducers/appSlice"; import { lessonsApi } from "core/services/lessons"; +import { teamApi } from "core/services/team"; const makeStore = (/* preloadedState */) => { const store = configureStore({ @@ -12,11 +13,13 @@ const makeStore = (/* preloadedState */) => { sideMenu: sideMenuSlice.reducer, lessonPageEditor: lessonPageEditorSlice.reducer, [lessonsApi.reducerPath]: lessonsApi.reducer, + [teamApi.reducerPath]: teamApi.reducer, }, // preloadedState, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat( - lessonsApi.middleware + lessonsApi.middleware, + teamApi.middleware ), }); diff --git a/src/core/types/lesson.ts b/src/core/types/lesson.ts index 64b3e0c..738259a 100644 --- a/src/core/types/lesson.ts +++ b/src/core/types/lesson.ts @@ -7,16 +7,25 @@ export interface Lesson { CreatedAt: string; } +export enum LessonState { + Published = 1, + Draft = 2, +} + // used for the preview card on /lessions page and on the lesson editor -export interface LessonPreview { +export interface LessonSettings { Title: string; ThumbnailUrl: string; + State?: LessonState; } // used on lesson page and on the lesson editor export interface LessonContent { Id: string; - Title: string; + Page: number; + Position: number; + Type: number; + Data: string; } export interface UpdateLessonPreviewThumbnail { diff --git a/src/core/types/team.ts b/src/core/types/team.ts new file mode 100644 index 0000000..00a559e --- /dev/null +++ b/src/core/types/team.ts @@ -0,0 +1,8 @@ +export interface TeamMember { + Id: string; + FirstName: string; + LastName: string; + Email: string; + Role: string; + Status: string; +} diff --git a/src/core/utils/utils.tsx b/src/core/utils/utils.tsx index b8d8fb9..36e2b57 100644 --- a/src/core/utils/utils.tsx +++ b/src/core/utils/utils.tsx @@ -14,8 +14,10 @@ export const Constants = { PAGE_EDITOR: "/lessons/:lessonId/editor", }, ORGANIZATION_TEAM: "/team", + ORGANIZATION_TEAM_CREATE_USER: "/team/create-user", ORGANIZATION_ROLES: "/roles", ORGANIZATION_SETTINGS: "/organization", + ACCOUNT_SETTINGS: "/account", WHATS_NEW: "/whats-new", SUGGEST_FEATURE: "/suggest-feature", CONTACT_SUPPORT: "/contact-support", diff --git a/src/features/AccountSettings/index.tsx b/src/features/AccountSettings/index.tsx new file mode 100644 index 0000000..59c40a9 --- /dev/null +++ b/src/features/AccountSettings/index.tsx @@ -0,0 +1,179 @@ +import { + Avatar, + Button, + Card, + Divider, + Flex, + Form, + Input, + Typography, +} from "antd"; +import HeaderBar from "../../core/components/Header"; +import MyBanner from "../../shared/components/MyBanner"; +import { MyContainer } from "../../shared/components/MyContainer"; +import { SaveOutlined } from "@ant-design/icons"; +import MyUpload from "shared/components/MyUpload"; +import { Constants } from "core/utils/utils"; +import ColorPicker from "antd/es/color-picker"; +import MyMiddleCard from "shared/components/MyMiddleCard"; +import Meta from "antd/es/card/Meta"; + +export function AccountSettingsAdmin() { + return ( + <> + } + /> + + + + + + } + title="Jorg Kreith" + description="Lead" + /> + + + +
+ + + + + + + + + + + + + + + + + + +
+
+
+
+ + ); +} + +export default function AccountSettings() { + return ( + <> + } + /> + + + + + + } + title="Jorg Kreith" + description="Lead" + /> + + + + + + + First name + Jorg + + + Email + julian@xx.com + + + + + Last name + Kreth + + + + + + + + ); +} + +/* +// TODO: sessions table + + + + // Todoo + +*/ + +function TitleText({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} + +function ValueText({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/src/features/Lessons/LessonPage/index.tsx b/src/features/Lessons/LessonPage/index.tsx index 4410e07..ad7f270 100644 --- a/src/features/Lessons/LessonPage/index.tsx +++ b/src/features/Lessons/LessonPage/index.tsx @@ -1,5 +1,4 @@ -import { Button, Card, Flex, Typography } from "antd"; -import { MyContainer } from "../../../shared/components/MyContainer"; +import { Button, Flex } from "antd"; import { CheckOutlined } from "@ant-design/icons"; import HeaderBar from "../../../core/components/Header"; import { useLocation, useNavigate, useParams } from "react-router-dom"; @@ -9,108 +8,8 @@ import MySpin from "shared/components/MySpin"; import MyErrorResult from "shared/components/MyResult"; import MyEmpty from "shared/components/MyEmpty"; import { useGetLessonContentsQuery } from "core/services/lessons"; - -export type LessonContent = { - id: string; - position: number; - type: number; - data: string; -}; - -/* -const LessonContents = [ - { - id: "0", - position: 1, - type: 0, - data: "How to clean the coffee machine", - }, - { - id: "1", - position: 1, - type: 2, - data: img, - }, - { - id: "2", - position: 2, - type: 1, - data: "The proper cleaning of the coffee machine", - }, - { - id: "3", - position: 3, - type: 1, - data: "Think a moment in silence. What makes you really happy? Are you the only one with this? Probably you could sell this knowledge to others! Think about it", - }, -] as LessonContent[]; */ - -export function Converter({ - mode, - lessonContent, - onEdit, -}: { - mode: "view" | "edititable"; - lessonContent: LessonContent; - onEdit?: (newData: string) => void; -}) { - // const dispatch = useDispatch(); - // const contents = useSelector(lessonContents); - - switch (lessonContent.type) { - case 0: - return mode === "view" ? ( -
- {lessonContent.data} -
- ) : ( - onEdit?.(event), - }} - level={1} - style={{ - margin: 0, - width: "100%", - }} - > - {lessonContent.data} - - ); - case 1: - return mode === "view" ? ( -
{lessonContent.data}
- ) : ( - onEdit?.(event), - }} - style={{ - margin: 0, - width: "100%", - }} - > - {lessonContent.data} - - ); - case 2: - return ( - img - ); - case 3: - return ( -
- {lessonContent.data} -
- ); - case 4: - return
{lessonContent.data}
; - default: - return
Unknown type
; - } -} +import MyMiddleCard from "shared/components/MyMiddleCard"; +import { Converter } from "../converter"; const LessonContents: React.FC = () => { const { lessonId } = useParams(); @@ -130,7 +29,7 @@ const LessonContents: React.FC = () => { return ( <> {data.map((lessonContent) => ( -
+
))} @@ -150,24 +49,15 @@ export default function LessonPage() { onEdit={() => navigate(`${location.pathname}/editor`)} /> - - - - + + - - - - + + - + ); } diff --git a/src/features/Lessons/LessonPageEditor/Droppable.tsx b/src/features/Lessons/LessonPageEditor/Droppable.tsx index 130d6cb..a49f2a5 100644 --- a/src/features/Lessons/LessonPageEditor/Droppable.tsx +++ b/src/features/Lessons/LessonPageEditor/Droppable.tsx @@ -1,52 +1,87 @@ import { DndContext, DragEndEvent, useDroppable } from "@dnd-kit/core"; -import { verticalListSortingStrategy, SortableContext } from "@dnd-kit/sortable"; +import { + verticalListSortingStrategy, + SortableContext, +} from "@dnd-kit/sortable"; import SortableEditorItem from "./SortableEditorItem"; -import React from "react"; -import { LessonContent } from "../LessonPage"; import { store } from "core/store/store"; import { restrictToVerticalAxis, restrictToWindowEdges, } from "@dnd-kit/modifiers"; -import { lessonContents, onDragHandler } from "./lessonPageEditorSlice"; +import { currentLessonId, onDragHandler } from "./lessonPageEditorSlice"; +import { LessonContent } from "core/types/lesson"; +import { useUpdateLessonContentPositionMutation } from "core/services/lessons"; +import { useSelector } from "react-redux"; const Droppable = ({ items }: { items: LessonContent[] }) => { const droppableID = "editorComponentArea"; const { setNodeRef } = useDroppable({ id: droppableID }); - + const currentLnId = useSelector(currentLessonId); - const itemIDs = items.map((item) => item.id); + const [reqUpdateLessonContentPosition] = + useUpdateLessonContentPositionMutation(); + + const itemIDs = items.map((item) => item.Id); + + const handleDragEnd = (event: DragEndEvent) => { + console.log("drag end", event); + + if (!event.over) return; + + const activeId = event.active.id; + const overId = event.over.id; + + if (activeId === overId) return; + + let oldIndex = itemIDs.findIndex((item) => item === activeId); + let newIndex = itemIDs.findIndex((item) => item === overId); + + // store.dispatch(onDragHandler({ activeId, overId })); + + store.dispatch(onDragHandler({ oldIndex, newIndex })); + + try { + reqUpdateLessonContentPosition({ + lessonId: currentLnId, + contentId: activeId, + newPosition: newIndex + 1, + }); + } catch (err) { + console.error(err); + } + }; return ( - + -
+
{items.map((item) => ( - + ))}
); }; - +/* function handleDragEnd(event: DragEndEvent) { - console.log("drag end",event); + console.log("drag end", event); - if(!event.over) return; + if (!event.over) return; const activeId = event.active.id; const overId = event.over.id; - - - store.dispatch(onDragHandler({activeId, overId})); -} - + store.dispatch(onDragHandler({ activeId, overId })); +} */ export default Droppable; diff --git a/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx b/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx index b280b33..f234cb0 100644 --- a/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx +++ b/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx @@ -1,55 +1,108 @@ -import React from "react"; import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; -import { Converter, LessonContent } from "../LessonPage"; -import { HolderOutlined, DeleteOutlined } from "@ant-design/icons"; +import { + HolderOutlined, + DeleteOutlined, + CameraOutlined, + FolderOpenOutlined, +} from "@ant-design/icons"; import { Flex } from "antd"; -import { setLessonContent, deleteLessonContent } from "./lessonPageEditorSlice"; -import { useDispatch } from "react-redux"; +import { + currentLessonId, + deleteLessonContent, + updateLessonContent, +} from "./lessonPageEditorSlice"; +import { useDispatch, useSelector } from "react-redux"; +import { getComponentByType } from "../components"; +import { LessonContent } from "core/types/lesson"; +import "./styles.module.css"; +import { Converter } from "../converter"; +import { useDeleteLessonContentMutation } from "core/services/lessons"; const animateLayoutChanges = (args: any) => args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true; -const SortableEditorItem = (props: {item:LessonContent}) => { +const SortableEditorItem = (props: { item: LessonContent }) => { const lnContent = props.item; - const { - attributes, - listeners, - setNodeRef, - transform, - transition - } = useSortable({ id: lnContent.id, animateLayoutChanges }); + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: lnContent.Id, animateLayoutChanges }); const dispatch = useDispatch(); - + + const component = getComponentByType(lnContent.Type); + + const [reqDeleteLessonContent] = useDeleteLessonContentMutation(); + const currentLnId = useSelector(currentLessonId); + + if (!component) { + return null; + } return ( -
- - - - dispatch( - setLessonContent({ - id: lnContent.id, - data: data, - }) - ) - } - /> + transition, + }} + ref={setNodeRef} + {...attributes} + > + + + { + console.log("edit", lnContent.Id, data); + dispatch( + updateLessonContent({ + id: lnContent.Id, + data: data, + }) + ); + }} + /> + + + {component.uploadImage ? ( +
+ +
+ ) : null} + {component.uploadFileTypes ? ( +
+ {" "} +
+ ) : null} +
{ - console.log("delete", lnContent.id); - dispatch(deleteLessonContent(lnContent.id)); + console.log("delete", lnContent.Id); + dispatch(deleteLessonContent(lnContent.Id)); + + try { + reqDeleteLessonContent({ + lessonId: currentLnId, + contentId: lnContent.Id, + }); + } catch (err) { + console.error(err); + } }} /> +
+
); }; diff --git a/src/features/Lessons/LessonPageEditor/index.tsx b/src/features/Lessons/LessonPageEditor/index.tsx index a004b8e..61db18d 100644 --- a/src/features/Lessons/LessonPageEditor/index.tsx +++ b/src/features/Lessons/LessonPageEditor/index.tsx @@ -2,34 +2,43 @@ import { useNavigate, useParams } from "react-router-dom"; import { lessonContents, lessonThumbnail, + setCurrentLessonId, setEditorActive, + setLessonContents, + setLessonState, } from "./lessonPageEditorSlice"; import { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { LessonContent } from "../LessonPage"; import { Card, Flex } from "antd"; -import { Constants } from "../../../core/utils/utils"; -import HeaderBar from "../../../core/components/Header"; +import { Constants } from "core/utils/utils"; +import HeaderBar from "core/components/Header"; import Droppable from "./Droppable"; -import LessonPreviewCard from "../../../shared/components/MyLessonPreviewCard"; +import LessonPreviewCard from "shared/components/MyLessonPreviewCard"; import { - useGetLessonPreviewQuery, + useGetLessonContentsQuery, + useGetLessonSettingsQuery, useUpdateLessonPreviewTitleMutation, } from "core/services/lessons"; import MyErrorResult from "shared/components/MyResult"; import styles from "./styles.module.css"; +import MyEmpty from "shared/components/MyEmpty"; const PreviewCard: React.FC = () => { + const dispatch = useDispatch(); const { lessonId } = useParams(); - const { data, error, isLoading, refetch } = useGetLessonPreviewQuery( + const { data, error, isLoading, refetch } = useGetLessonSettingsQuery( lessonId as string, { refetchOnMountOrArgChange: true, } ); - const [updateLessonPreviewTitle, {}] = useUpdateLessonPreviewTitleMutation(); + const [updateLessonPreviewTitle] = useUpdateLessonPreviewTitleMutation(); + + useEffect(() => { + if (data?.State) dispatch(setLessonState(data.State)); + }, [data]); if (error) return ; @@ -38,7 +47,7 @@ const PreviewCard: React.FC = () => { mode="editable" lessonId={lessonId as string} loading={isLoading} - lessonPreview={{ + lessonSettings={{ Title: data?.Title || "", ThumbnailUrl: data?.ThumbnailUrl || "", }} @@ -61,6 +70,40 @@ const PreviewCard: React.FC = () => { ); }; +const LessonContentComponents: React.FC = () => { + const { lessonId } = useParams(); + const dispatch = useDispatch(); + + const { data, error, isLoading } = useGetLessonContentsQuery( + lessonId as string, + { + refetchOnMountOrArgChange: true, + } + ); + + const lnContents = useSelector(lessonContents); + + useEffect(() => { + if (!data) return; + + dispatch(setLessonContents(data)); + }, [data]); + + if (error) return ; + + return ( + + + {!lnContents || lnContents.length == 0 ? ( + + ) : ( + + )} + + + ); +}; + export default function LessonPageEditor() { const { lessonId } = useParams(); const navigate = useNavigate(); @@ -71,6 +114,7 @@ export default function LessonPageEditor() { useEffect(() => { dispatch(setEditorActive(true)); + dispatch(setCurrentLessonId(lessonId as string)); return () => { dispatch(setEditorActive(false)); @@ -104,22 +148,9 @@ export default function LessonPageEditor() { > - - - - - + ); -} - -export function StandardEditorCompontent(type: number): LessonContent { - return { - id: Math.floor(Math.random() * 10000).toString(), - position: 1, - type: type, - data: "Some data", - }; -} +} \ No newline at end of file diff --git a/src/features/Lessons/LessonPageEditor/lessonPageEditorSlice.tsx b/src/features/Lessons/LessonPageEditor/lessonPageEditorSlice.tsx index 94bc0c2..c013869 100644 --- a/src/features/Lessons/LessonPageEditor/lessonPageEditorSlice.tsx +++ b/src/features/Lessons/LessonPageEditor/lessonPageEditorSlice.tsx @@ -1,76 +1,89 @@ import { createSlice } from "@reduxjs/toolkit"; -import { LessonContent } from "../LessonPage"; -import { StandardEditorCompontent } from "."; +import { LessonContent, LessonState } from "core/types/lesson"; + +interface AddLessonContentAction { + type: string; + payload: LessonContent; +} export const lessonPageEditorSlice = createSlice({ name: "lessonPageEditor", initialState: { editorActive: false, + currentLessonId: "", // required in sideMenu because has no access to useParams lessonThumbnail: { img: "", title: "Test", }, lessonContents: [] as LessonContent[], + lessonState: LessonState.Draft, }, reducers: { setEditorActive: (state, action) => { state.editorActive = action.payload; }, - addLessonContent: (state, action) => { - state.lessonContents.push(StandardEditorCompontent(action.payload)); + setCurrentLessonId: (state, action) => { + state.currentLessonId = action.payload; + }, + addLessonContent: (state, action: AddLessonContentAction) => { + state.lessonContents.push(action.payload); }, deleteLessonContent: (state, action) => { state.lessonContents = state.lessonContents.filter( - (content) => content.id !== action.payload + (content) => content.Id !== action.payload ); }, - setLessonContent: (state, action) => { + setLessonContents: (state, action) => { + state.lessonContents = action.payload; + }, + updateLessonContent: (state, action) => { const index = state.lessonContents.findIndex( - (content) => content.id === action.payload.id + (content) => content.Id === action.payload.id ); - state.lessonContents[index].data = action.payload.data; + if (index !== -1) { + state.lessonContents[index].Data = action.payload.data; + } }, setLessonThumbnailTitle: (state, action) => { state.lessonThumbnail.title = action.payload; }, onDragHandler: (state, action) => { - const { activeId, overId } = action.payload as { - activeId: string; - overId: string; - }; - - if (activeId !== overId) { - let oldIndex = state.lessonContents.findIndex( - (item) => item.id === activeId - ); - let newIndex = state.lessonContents.findIndex( - (item) => item.id === overId - ); - - state.lessonContents.splice( - newIndex, - 0, - state.lessonContents.splice(oldIndex, 1)[0] - ); - } + state.lessonContents.splice( + action.payload.newIndex, + 0, + state.lessonContents.splice(action.payload.oldIndex, 1)[0] + ); + }, + setLessonState: (state, action) => { + state.lessonState = action.payload; }, }, selectors: { editorActive: (state) => state.editorActive, + currentLessonId: (state) => state.currentLessonId, lessonContents: (state) => state.lessonContents, lessonThumbnail: (state) => state.lessonThumbnail, + lessonState: (state) => state.lessonState, }, }); export const { setEditorActive, + setCurrentLessonId, addLessonContent, deleteLessonContent, - setLessonContent, + setLessonContents, + updateLessonContent, setLessonThumbnailTitle, onDragHandler, + setLessonState, } = lessonPageEditorSlice.actions; -export const { editorActive, lessonContents, lessonThumbnail } = - lessonPageEditorSlice.selectors; +export const { + editorActive, + currentLessonId, + lessonContents, + lessonThumbnail, + lessonState, +} = lessonPageEditorSlice.selectors; diff --git a/src/features/Lessons/LessonPageEditor/styles.module.css b/src/features/Lessons/LessonPageEditor/styles.module.css index 9b6a368..9939b6f 100644 --- a/src/features/Lessons/LessonPageEditor/styles.module.css +++ b/src/features/Lessons/LessonPageEditor/styles.module.css @@ -8,3 +8,8 @@ max-width: 800px; } } + +.EditorActionIcon { + cursor: pointer; + padding: 8px; +} diff --git a/src/features/Lessons/components.ts b/src/features/Lessons/components.ts index cb2a596..4338cac 100644 --- a/src/features/Lessons/components.ts +++ b/src/features/Lessons/components.ts @@ -10,6 +10,9 @@ export type Component = { name: string; thumbnail?: string; invertThumbnailAtDarkmode?: boolean; + uploadFileTypes?: string[]; + uploadImage?: boolean; + defaultData?: string; }; const componentsGroups: ComponentGroup[] = [ @@ -21,12 +24,14 @@ const componentsGroups: ComponentGroup[] = [ name: "Header", thumbnail: "/editor/thumbnails/component_thumbnail_header.svg", invertThumbnailAtDarkmode: true, + defaultData: "Header", }, { type: 1, name: "Text", thumbnail: "/editor/thumbnails/component_thumbnail_text.svg", invertThumbnailAtDarkmode: true, + defaultData: "Text", }, ], }, @@ -34,18 +39,20 @@ const componentsGroups: ComponentGroup[] = [ category: "Media", components: [ { - type: 0, + type: 2, name: "Image", thumbnail: "/editor/thumbnails/component_thumbnail_image.png", + uploadImage: true, + uploadFileTypes: ["image/*"], }, { - type: 1, + type: 3, name: "YouTube", thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png", invertThumbnailAtDarkmode: true, }, { - type: 1, + type: 4, name: "Video", thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png", invertThumbnailAtDarkmode: true, @@ -56,7 +63,7 @@ const componentsGroups: ComponentGroup[] = [ category: "HTML", components: [ { - type: 0, + type: 5, name: "Iframe", thumbnail: "/editor/thumbnails/component_thumbnail_iframe.svg", invertThumbnailAtDarkmode: true, @@ -67,7 +74,7 @@ const componentsGroups: ComponentGroup[] = [ category: "Special", components: [ { - type: 0, + type: 6, name: "Banner", thumbnail: "/editor/thumbnails/component_thumbnail_banner.png", }, @@ -75,4 +82,31 @@ const componentsGroups: ComponentGroup[] = [ }, ]; +const componentsMap: { [key: string]: Component } = (() => { + const map: { [key: string]: Component } = {}; + + for (const group of componentsGroups) { + for (const component of group.components) { + map[component.name] = component; + } + } + return map; +})(); + +export function getTypeByName(name: string): number { + const component = componentsMap[name]; + + return component ? component.type : -1; +} + +export function getComponentByType(type: number): Component | null { + for (const component of Object.values(componentsMap)) { + if (component.type === type) { + return component; + } + } + + return null; +} + export { componentsGroups }; diff --git a/src/features/Lessons/converter.tsx b/src/features/Lessons/converter.tsx new file mode 100644 index 0000000..eb9a8bd --- /dev/null +++ b/src/features/Lessons/converter.tsx @@ -0,0 +1,183 @@ +import { LessonContent } from "core/types/lesson"; +import { getTypeByName } from "./components"; +import { Button, Input, Typography } from "antd"; +import { useUpdateLessonContentMutation } from "core/services/lessons"; +import { useSelector } from "react-redux"; +import { currentLessonId } from "./LessonPageEditor/lessonPageEditorSlice"; +import { useRef } from "react"; +import MyUpload from "shared/components/MyUpload"; + +export function Converter({ + mode, + lessonContent, + onEdit, +}: { + mode: "view" | "edititable"; + lessonContent: LessonContent; + onEdit?: (newData: string) => void; +}) { + const lessonId = useSelector(currentLessonId); + + const [reqUpdateLessonContent] = useUpdateLessonContentMutation(); + + const debounceRef = useRef(null); + + switch (lessonContent.Type) { + case getTypeByName("Header"): + if (mode === "view") { + return ( +
+ {lessonContent.Data} +
+ ); + } + + return ( + { + onEdit?.(event); + + try { + reqUpdateLessonContent({ + lessonId: lessonId, + contentId: lessonContent.Id, + data: event, + }); + } catch (err) { + console.error(err); + } + }, + }} + level={1} + style={{ + margin: 0, + width: "100%", + }} + > + {lessonContent.Data} + + ); + case getTypeByName("Text"): + if (mode === "view") { + return ( +
+ {lessonContent.Data} +
+ ); + } + + return ( + { + console.log("edit"); + + onEdit?.(event.target.value); + + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + + debounceRef.current = setTimeout(() => { + try { + reqUpdateLessonContent({ + lessonId: lessonId, + contentId: lessonContent.Id, + data: event.target.value, + }); + } catch (err) { + console.error(err); + } + }, 1000); + }} + /> + ); + case getTypeByName("Image"): + console.log("image", lessonContent.Data); + + if (mode === "view" && lessonContent.Data === "") { + return ( +
+
+ No image provided +
+
+ ); + } + + if (lessonContent.Data === "") { + return ( +
+
+ Choose image from + + + + +
+
+ ); + } + + return ( + <> + img + + ); + case getTypeByName("YouTube"): + return ( +
+ {lessonContent.Data} +
+ ); + case getTypeByName("Video"): + return
Not implemented
; + case getTypeByName("Iframe"): + return
Not implemented
; + case getTypeByName("Banner"): + return
Not implemented
; + + default: + return
Unknown type
; + } +} diff --git a/src/features/Lessons/index.tsx b/src/features/Lessons/index.tsx index c948c96..f06f242 100644 --- a/src/features/Lessons/index.tsx +++ b/src/features/Lessons/index.tsx @@ -1,6 +1,6 @@ -import { Button, Flex, Segmented } from "antd"; -import MyBanner from "../../shared/components/MyBanner"; -import { MyContainer } from "../../shared/components/MyContainer"; +import { Button, Divider, Flex, Segmented } from "antd"; +import MyBanner from "shared/components/MyBanner"; +import { MyContainer } from "shared/components/MyContainer"; import { AppstoreOutlined, BarsOutlined, @@ -8,7 +8,7 @@ import { } from "@ant-design/icons"; import Search, { SearchProps } from "antd/es/input/Search"; import { useNavigate } from "react-router-dom"; -import HeaderBar from "../../core/components/Header"; +import HeaderBar from "core/components/Header"; import { useCreateLessonMutation, useGetLessonsQuery, @@ -17,7 +17,7 @@ import MySpin from "shared/components/MySpin"; import { Constants } from "core/utils/utils"; import MyEmpty from "shared/components/MyEmpty"; import MyErrorResult from "shared/components/MyResult"; -import LessonPreviewCard from "../../shared/components/MyLessonPreviewCard"; +import LessonPreviewCard from "shared/components/MyLessonPreviewCard"; const CreateLessonButton: React.FC = () => { const navigate = useNavigate(); @@ -61,20 +61,46 @@ const LessonList: React.FC = () => { if (!data || data.length === 0) return ; + const publishedItems = data.filter(item => item.State === 1); + const unpublishedItems = data.filter(item => item.State === 2); + return ( <> - {data.map((item, index) => ( - - ))} + {publishedItems.length > 0 && ( + <> + {publishedItems.map((item, index) => ( + + ))} + + )} + + {unpublishedItems.length > 0 && ( + <> + Unpublished + + {unpublishedItems.map((item, index) => ( + + ))} + + )} ); }; diff --git a/src/features/Settings/index.tsx b/src/features/Settings/index.tsx index 0c1b22a..47a1aff 100644 --- a/src/features/Settings/index.tsx +++ b/src/features/Settings/index.tsx @@ -1,16 +1,137 @@ +import { Button, Card, Divider, Flex, Form, Input, Typography } from "antd"; import HeaderBar from "../../core/components/Header"; import MyBanner from "../../shared/components/MyBanner"; import { MyContainer } from "../../shared/components/MyContainer"; +import { SaveOutlined } from "@ant-design/icons"; +import MyUpload from "shared/components/MyUpload"; +import { Constants } from "core/utils/utils"; +import ColorPicker from "antd/es/color-picker"; export default function Settings() { return ( <> - - } /> + } /> -

Settings

+ + + +
+ + + Branding + + +
+ + + + ); +} diff --git a/src/features/Team/index.tsx b/src/features/Team/index.tsx index e58da86..5bcc662 100644 --- a/src/features/Team/index.tsx +++ b/src/features/Team/index.tsx @@ -1,16 +1,101 @@ +import MyTable from "shared/components/MyTable"; import HeaderBar from "../../core/components/Header"; import MyBanner from "../../shared/components/MyBanner"; import { MyContainer } from "../../shared/components/MyContainer"; +import { Button, Flex } from "antd"; +import { UserAddOutlined } from "@ant-design/icons"; +import { Link } from "react-router-dom"; +import { Constants } from "core/utils/utils"; +import { useGetTeamQuery } from "core/services/team"; +import MyErrorResult from "shared/components/MyResult"; + +const TeamList: React.FC = () => { + const { data, error, isLoading } = useGetTeamQuery(undefined, { + refetchOnMountOrArgChange: true, + }); + + const getTableContent = () => { + let items = [ + { + title: "First name", + dataIndex: "firstName", + key: "firstName", + }, + { + title: "Last name", + dataIndex: "lastName", + key: "lastName", + }, + { + title: "Email", + dataIndex: "email", + key: "email", + }, + { + title: "Role", + dataIndex: "role", + key: "role", + }, + { + title: "Status", + dataIndex: "status", + key: "status", + }, + { + title: "Actions", + dataIndex: "actions", + key: "actions", + }, + ]; + + return items; + }; + + const getTableItems = () => { + let items = [] as any[]; + + if (!data) return items; + + data.forEach((item) => { + items.push({ + key: item.Id, + firstName: item.FirstName, + lastName: item.LastName, + email: item.Email, + role: item.Role, + status: item.Status, + }); + }); + + return items; + } + + if (error) return ; + + return ( + + ); +}; export default function Team() { return ( <> - - } /> + } /> - -

Team

+ + + + + + + + ); diff --git a/src/shared/components/MyContainer/index.tsx b/src/shared/components/MyContainer/index.tsx index 173eff7..da57852 100644 --- a/src/shared/components/MyContainer/index.tsx +++ b/src/shared/components/MyContainer/index.tsx @@ -1,7 +1,13 @@ import { Content } from "antd/es/layout/layout"; -export function MyContainer({ children }: { children: React.ReactNode }) { - return {children}; +export function MyContainer({ + children, + style = {}, +}: { + children: React.ReactNode; + style?: React.CSSProperties; +}) { + return {children}; } export function MyCenteredContainer({ diff --git a/src/shared/components/MyLessonPreviewCard/index.tsx b/src/shared/components/MyLessonPreviewCard/index.tsx index beb1906..4adc689 100644 --- a/src/shared/components/MyLessonPreviewCard/index.tsx +++ b/src/shared/components/MyLessonPreviewCard/index.tsx @@ -1,23 +1,23 @@ import { CommentOutlined } from "@ant-design/icons"; import { Card, Flex, Typography } from "antd"; -import { LessonPreview } from "core/types/lesson"; import { Constants, getImageUrl } from "core/utils/utils"; import { Link } from "react-router-dom"; import MyUpload from "shared/components/MyUpload"; import styles from "./styles.module.css"; +import { LessonSettings } from "core/types/lesson"; export default function MyLessonPreviewCard({ mode = "view", lessonId, loading = false, - lessonPreview, + lessonSettings, onEditTitle, onThumbnailChanged, }: { mode: "view" | "editable"; lessonId: string; loading?: boolean; - lessonPreview: LessonPreview; + lessonSettings: LessonSettings; onEditTitle?: (newTitle: string) => void; onThumbnailChanged?: () => void; }) { @@ -61,9 +61,9 @@ export default function MyLessonPreviewCard({ lesson thumbnail {mode === "view" ? (
-
{lessonPreview.Title}
+
{lessonSettings.Title}
12 comments
) : ( @@ -88,7 +88,7 @@ export default function MyLessonPreviewCard({ width: "100%", }} > - {lessonPreview.Title} + {lessonSettings.Title} )}
diff --git a/src/shared/components/MyMiddleCard/index.tsx b/src/shared/components/MyMiddleCard/index.tsx new file mode 100644 index 0000000..bdb62e6 --- /dev/null +++ b/src/shared/components/MyMiddleCard/index.tsx @@ -0,0 +1,26 @@ +import { Card, CardProps, Flex } from "antd"; +import { MyContainer } from "../MyContainer"; + +interface MyMiddleCardProps extends CardProps {} + +const MyMiddleCard: React.FC = ({ children, + ...props + }) => { + return ( + + + + {children} + + + + ); +} + +export default MyMiddleCard; \ No newline at end of file diff --git a/src/shared/components/MyTable/index.tsx b/src/shared/components/MyTable/index.tsx new file mode 100644 index 0000000..216c399 --- /dev/null +++ b/src/shared/components/MyTable/index.tsx @@ -0,0 +1,9 @@ +import { Table, TableProps } from "antd"; + +interface MyTableProps extends TableProps {} + +const MyTable: React.FC = (props) => { + return ; +}; + +export default MyTable; diff --git a/src/shared/components/MyUpload/index.tsx b/src/shared/components/MyUpload/index.tsx index 7bd45a1..45df9f7 100644 --- a/src/shared/components/MyUpload/index.tsx +++ b/src/shared/components/MyUpload/index.tsx @@ -49,6 +49,7 @@ export default function MyUpload({ action={`${Constants.API_ADDRESS}${action}`} onChange={onChange} beforeUpload={beforeUpload} + > {children}