editor, drag and drop, settings organization and account
parent
23b9a7e249
commit
00dee1ba9e
|
@ -11,6 +11,8 @@ import Settings from "../../../features/Settings";
|
||||||
import PageNotFound from "../../../features/PageNotFound";
|
import PageNotFound from "../../../features/PageNotFound";
|
||||||
import LessonPage from "../../../features/Lessons/LessonPage";
|
import LessonPage from "../../../features/Lessons/LessonPage";
|
||||||
import LessonPageEditor from "../../../features/Lessons/LessonPageEditor";
|
import LessonPageEditor from "../../../features/Lessons/LessonPageEditor";
|
||||||
|
import TeamCreateUser from "features/Team/CreateUser";
|
||||||
|
import AccountSettings from "features/AccountSettings";
|
||||||
|
|
||||||
export default function AppRoutes() {
|
export default function AppRoutes() {
|
||||||
return (
|
return (
|
||||||
|
@ -51,6 +53,15 @@ export default function AppRoutes() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={Constants.ROUTE_PATHS.ORGANIZATION_TEAM_CREATE_USER}
|
||||||
|
element={
|
||||||
|
<MySupsenseFallback>
|
||||||
|
<TeamCreateUser />
|
||||||
|
</MySupsenseFallback>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={Constants.ROUTE_PATHS.ORGANIZATION_ROLES}
|
path={Constants.ROUTE_PATHS.ORGANIZATION_ROLES}
|
||||||
element={
|
element={
|
||||||
|
@ -60,7 +71,7 @@ export default function AppRoutes() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={Constants.ROUTE_PATHS.ORGANIZATION_SETTINGS}
|
path={Constants.ROUTE_PATHS.ORGANIZATION_SETTINGS}
|
||||||
element={
|
element={
|
||||||
<MySupsenseFallback>
|
<MySupsenseFallback>
|
||||||
|
@ -69,6 +80,15 @@ export default function AppRoutes() {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={Constants.ROUTE_PATHS.ACCOUNT_SETTINGS}
|
||||||
|
element={
|
||||||
|
<MySupsenseFallback>
|
||||||
|
<AccountSettings />
|
||||||
|
</MySupsenseFallback>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={Constants.ROUTE_PATHS.WHATS_NEW}
|
path={Constants.ROUTE_PATHS.WHATS_NEW}
|
||||||
element={
|
element={
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { DraggableCreateComponent } from "../SideMenu";
|
||||||
import { componentsGroups } from "features/Lessons/components";
|
import { componentsGroups } from "features/Lessons/components";
|
||||||
|
|
||||||
function MyDndContext({ children }: { children: React.ReactNode }) {
|
function MyDndContext({ children }: { children: React.ReactNode }) {
|
||||||
return <DndContext collisionDetection={closestCenter}>{children}
|
return <DndContext >{children}
|
||||||
</DndContext>;
|
</DndContext>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ import {
|
||||||
TeamOutlined,
|
TeamOutlined,
|
||||||
WalletOutlined,
|
WalletOutlined,
|
||||||
} from "@ant-design/icons";
|
} 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 { 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 { useDispatch, useSelector } from "react-redux";
|
||||||
import {
|
import {
|
||||||
setIsSideMenuCollapsed,
|
setIsSideMenuCollapsed,
|
||||||
|
@ -20,16 +20,21 @@ import { ItemType, MenuItemType } from "antd/es/menu/interface";
|
||||||
import { BreakpointLgWidth, Constants } from "core/utils/utils";
|
import { BreakpointLgWidth, Constants } from "core/utils/utils";
|
||||||
import Search from "antd/es/input/Search";
|
import Search from "antd/es/input/Search";
|
||||||
import { MyContainer } from "shared/components/MyContainer";
|
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 { Component, componentsGroups } from "features/Lessons/components";
|
||||||
|
|
||||||
import { darkMode } from "core/reducers/appSlice";
|
import { darkMode } from "core/reducers/appSlice";
|
||||||
|
|
||||||
import { DndContext, DragOverlay, useDraggable } from "@dnd-kit/core";
|
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 { 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() {
|
export function SideMenuContent() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
@ -211,8 +216,25 @@ export function SideMenuContent() {
|
||||||
export function SideMenuEditorContent() {
|
export function SideMenuEditorContent() {
|
||||||
// create is dragging useState
|
// create is dragging useState
|
||||||
const [isDragging, setIsDragging] = useState<String | null>(null);
|
const [isDragging, setIsDragging] = useState<String | null>(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 (
|
return (
|
||||||
|
<Flex justify="space-between" vertical style={{ height: "100%" }}>
|
||||||
<MyContainer>
|
<MyContainer>
|
||||||
<Search
|
<Search
|
||||||
placeholder="What would you like to insert?"
|
placeholder="What would you like to insert?"
|
||||||
|
@ -265,6 +287,34 @@ export function SideMenuEditorContent() {
|
||||||
)}
|
)}
|
||||||
</DndContext>
|
</DndContext>
|
||||||
</MyContainer>
|
</MyContainer>
|
||||||
|
|
||||||
|
<div style={{ padding: 12 }}>
|
||||||
|
<Form form={form} layout="vertical">
|
||||||
|
<Form.Item label="Status" name="state" style={{ marginBottom: 0 }}>
|
||||||
|
<Select
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
onChange={async (value) => {
|
||||||
|
console.log("state changed", value, lessonId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateLessonState({
|
||||||
|
lessonId: currentLnId,
|
||||||
|
newState: value,
|
||||||
|
}).unwrap();
|
||||||
|
} catch (err) {
|
||||||
|
console.log("error", err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Select.Option value={LessonState.Published}>
|
||||||
|
Published
|
||||||
|
</Select.Option>
|
||||||
|
<Select.Option value={LessonState.Draft}>Draft</Select.Option>
|
||||||
|
</Select>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -275,27 +325,24 @@ export function DraggableCreateComponent({
|
||||||
}) {
|
}) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { attributes, listeners, setNodeRef, transform, isDragging, active } =
|
/*const { attributes, listeners, setNodeRef, transform, isDragging, active } =
|
||||||
useDraggable({
|
useDraggable({
|
||||||
id: "draggable_" + component.name,
|
id: "draggable_" + component.name,
|
||||||
});
|
});*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
ref={setNodeRef}
|
//ref={setNodeRef}
|
||||||
{...attributes}
|
//{...attributes}
|
||||||
{...listeners}
|
//{...listeners}
|
||||||
style={{
|
style={
|
||||||
transition: !isDragging ? "all 0.3s ease-out" : "none",
|
{
|
||||||
|
//transition: !isDragging ? "all 0.3s ease-out" : "none",
|
||||||
boxShadow: isDragging ? "0px 0px 10px 0px #00000040" : "none",
|
//boxShadow: isDragging ? "0px 0px 10px 0px #00000040" : "none",
|
||||||
opacity: isDragging ? 0.25 : 1,
|
//opacity: isDragging ? 0.25 : 1,
|
||||||
}}
|
}
|
||||||
onClick={() => {
|
}
|
||||||
console.log("insert component", component.type);
|
|
||||||
dispatch(addLessonContent(component.type));
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<CreateComponent component={component} />
|
<CreateComponent component={component} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -307,6 +354,10 @@ function CreateComponent({ component }: { component: Component }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const isDarkMode = useSelector(darkMode);
|
const isDarkMode = useSelector(darkMode);
|
||||||
|
|
||||||
|
const currentLnId = useSelector(currentLessonId);
|
||||||
|
|
||||||
|
const [reqAddLessonContent] = useAddLessonContentMutation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
vertical
|
vertical
|
||||||
|
@ -318,9 +369,30 @@ function CreateComponent({ component }: { component: Component }) {
|
||||||
width: 80,
|
width: 80,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
console.log("insert component", component.type);
|
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 ? (
|
{component.thumbnail ? (
|
||||||
|
|
|
@ -2,10 +2,10 @@ import { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
import { baseQueryWithErrorHandling } from "core/helper/api";
|
import { baseQueryWithErrorHandling } from "core/helper/api";
|
||||||
import {
|
import {
|
||||||
Lesson,
|
Lesson,
|
||||||
LessonPreview,
|
LessonContent,
|
||||||
|
LessonSettings,
|
||||||
UpdateLessonPreviewThumbnail,
|
UpdateLessonPreviewThumbnail,
|
||||||
} from "core/types/lesson";
|
} from "core/types/lesson";
|
||||||
import { LessonContent } from "features/Lessons/LessonPage";
|
|
||||||
|
|
||||||
export const lessonsApi = createApi({
|
export const lessonsApi = createApi({
|
||||||
reducerPath: "lessonsApi",
|
reducerPath: "lessonsApi",
|
||||||
|
@ -23,9 +23,9 @@ export const lessonsApi = createApi({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
getLessonPreview: builder.query<LessonPreview, string>({
|
getLessonSettings: builder.query<LessonSettings, string>({
|
||||||
query: (lessonId) => ({
|
query: (lessonId) => ({
|
||||||
url: `lessons/${lessonId}/preview`,
|
url: `lessons/${lessonId}/settings`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
@ -52,14 +52,60 @@ export const lessonsApi = createApi({
|
||||||
body: formData,
|
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 {
|
export const {
|
||||||
useGetLessonsQuery,
|
useGetLessonsQuery,
|
||||||
useCreateLessonMutation,
|
useCreateLessonMutation,
|
||||||
useGetLessonPreviewQuery,
|
useGetLessonSettingsQuery,
|
||||||
useGetLessonContentsQuery,
|
useGetLessonContentsQuery,
|
||||||
useUpdateLessonPreviewTitleMutation,
|
useUpdateLessonPreviewTitleMutation,
|
||||||
useUpdateLessonPreviewThumbnailMutation,
|
useUpdateLessonPreviewThumbnailMutation,
|
||||||
|
useUpdateLessonStateMutation,
|
||||||
|
useAddLessonContentMutation,
|
||||||
|
useUpdateLessonContentMutation,
|
||||||
|
useUpdateLessonContentPositionMutation,
|
||||||
|
useDeleteLessonContentMutation,
|
||||||
} = lessonsApi;
|
} = lessonsApi;
|
||||||
|
|
|
@ -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<TeamMember[], undefined>({
|
||||||
|
query: () => ({
|
||||||
|
url: "team/members",
|
||||||
|
method: "GET",
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useGetTeamQuery } = teamApi;
|
|
@ -4,6 +4,7 @@ import { sideMenuSlice } from "../components/SideMenu/sideMenuSlice";
|
||||||
import { lessonPageEditorSlice } from "../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
import { lessonPageEditorSlice } from "../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||||
import { appSlice } from "../reducers/appSlice";
|
import { appSlice } from "../reducers/appSlice";
|
||||||
import { lessonsApi } from "core/services/lessons";
|
import { lessonsApi } from "core/services/lessons";
|
||||||
|
import { teamApi } from "core/services/team";
|
||||||
|
|
||||||
const makeStore = (/* preloadedState */) => {
|
const makeStore = (/* preloadedState */) => {
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
|
@ -12,11 +13,13 @@ const makeStore = (/* preloadedState */) => {
|
||||||
sideMenu: sideMenuSlice.reducer,
|
sideMenu: sideMenuSlice.reducer,
|
||||||
lessonPageEditor: lessonPageEditorSlice.reducer,
|
lessonPageEditor: lessonPageEditorSlice.reducer,
|
||||||
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
||||||
|
[teamApi.reducerPath]: teamApi.reducer,
|
||||||
},
|
},
|
||||||
// preloadedState,
|
// preloadedState,
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) =>
|
||||||
getDefaultMiddleware().concat(
|
getDefaultMiddleware().concat(
|
||||||
lessonsApi.middleware
|
lessonsApi.middleware,
|
||||||
|
teamApi.middleware
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -7,16 +7,25 @@ export interface Lesson {
|
||||||
CreatedAt: string;
|
CreatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LessonState {
|
||||||
|
Published = 1,
|
||||||
|
Draft = 2,
|
||||||
|
}
|
||||||
|
|
||||||
// used for the preview card on /lessions page and on the lesson editor
|
// used for the preview card on /lessions page and on the lesson editor
|
||||||
export interface LessonPreview {
|
export interface LessonSettings {
|
||||||
Title: string;
|
Title: string;
|
||||||
ThumbnailUrl: string;
|
ThumbnailUrl: string;
|
||||||
|
State?: LessonState;
|
||||||
}
|
}
|
||||||
|
|
||||||
// used on lesson page and on the lesson editor
|
// used on lesson page and on the lesson editor
|
||||||
export interface LessonContent {
|
export interface LessonContent {
|
||||||
Id: string;
|
Id: string;
|
||||||
Title: string;
|
Page: number;
|
||||||
|
Position: number;
|
||||||
|
Type: number;
|
||||||
|
Data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateLessonPreviewThumbnail {
|
export interface UpdateLessonPreviewThumbnail {
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export interface TeamMember {
|
||||||
|
Id: string;
|
||||||
|
FirstName: string;
|
||||||
|
LastName: string;
|
||||||
|
Email: string;
|
||||||
|
Role: string;
|
||||||
|
Status: string;
|
||||||
|
}
|
|
@ -14,8 +14,10 @@ export const Constants = {
|
||||||
PAGE_EDITOR: "/lessons/:lessonId/editor",
|
PAGE_EDITOR: "/lessons/:lessonId/editor",
|
||||||
},
|
},
|
||||||
ORGANIZATION_TEAM: "/team",
|
ORGANIZATION_TEAM: "/team",
|
||||||
|
ORGANIZATION_TEAM_CREATE_USER: "/team/create-user",
|
||||||
ORGANIZATION_ROLES: "/roles",
|
ORGANIZATION_ROLES: "/roles",
|
||||||
ORGANIZATION_SETTINGS: "/organization",
|
ORGANIZATION_SETTINGS: "/organization",
|
||||||
|
ACCOUNT_SETTINGS: "/account",
|
||||||
WHATS_NEW: "/whats-new",
|
WHATS_NEW: "/whats-new",
|
||||||
SUGGEST_FEATURE: "/suggest-feature",
|
SUGGEST_FEATURE: "/suggest-feature",
|
||||||
CONTACT_SUPPORT: "/contact-support",
|
CONTACT_SUPPORT: "/contact-support",
|
||||||
|
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<MyBanner
|
||||||
|
title="Account Settings"
|
||||||
|
subtitle="MANAGE"
|
||||||
|
headerBar={<HeaderBar />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MyMiddleCard title="My Profile">
|
||||||
|
<Flex vertical gap={16} >
|
||||||
|
<Card
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Meta
|
||||||
|
avatar={
|
||||||
|
<Avatar
|
||||||
|
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
||||||
|
size={56}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Jorg Kreith"
|
||||||
|
description="Lead"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 16,
|
||||||
|
paddingBottom: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Meta title="Personal Information" />
|
||||||
|
<Form layout="vertical" style={{ marginTop: 24 }}>
|
||||||
|
<Flex gap={16}>
|
||||||
|
<Flex flex={1}>
|
||||||
|
<Form.Item label="First name" name="firstName" style={{width: "100%"}}>
|
||||||
|
<Input defaultValue="Jorg" />
|
||||||
|
</Form.Item>
|
||||||
|
</Flex>
|
||||||
|
<Flex flex={1}>
|
||||||
|
<Form.Item label="Last name" name="lastName" style={{width: "100%"}}>
|
||||||
|
<Input defaultValue="Kreth" />
|
||||||
|
</Form.Item>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Form.Item label="Email" name="email">
|
||||||
|
<Input defaultValue="julian@xx.com" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item style={{ textAlign: 'right' }}>
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
htmlType="submit"
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</MyMiddleCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AccountSettings() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MyBanner
|
||||||
|
title="Account Settings"
|
||||||
|
subtitle="MANAGE"
|
||||||
|
headerBar={<HeaderBar />}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MyMiddleCard title="My Profile">
|
||||||
|
<Flex vertical gap={16} style={{ paddingBottom: 16 }}>
|
||||||
|
<Card
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Meta
|
||||||
|
avatar={
|
||||||
|
<Avatar
|
||||||
|
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
||||||
|
size={56}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
title="Jorg Kreith"
|
||||||
|
description="Lead"
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<Card
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Meta title="Personal Information" />
|
||||||
|
<Flex gap={16} style={{ marginTop: 24 }}>
|
||||||
|
<Flex vertical gap={24} flex={1}>
|
||||||
|
<Flex gap={8} vertical>
|
||||||
|
<TitleText>First name</TitleText>
|
||||||
|
<ValueText>Jorg</ValueText>
|
||||||
|
</Flex>
|
||||||
|
<Flex gap={8} vertical>
|
||||||
|
<TitleText>Email</TitleText>
|
||||||
|
<ValueText>julian@xx.com</ValueText>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex vertical gap={24} flex={1}>
|
||||||
|
<Flex gap={8} vertical>
|
||||||
|
<TitleText>Last name</TitleText>
|
||||||
|
<ValueText>Kreth</ValueText>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</MyMiddleCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// TODO: sessions table
|
||||||
|
|
||||||
|
<Card
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Meta title="Sessions" />
|
||||||
|
<Typography.Text>// Todoo</Typography.Text>
|
||||||
|
</Card>
|
||||||
|
*/
|
||||||
|
|
||||||
|
function TitleText({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Typography.Text style={{ fontSize: 16, opacity: 0.4 }}>
|
||||||
|
{children}
|
||||||
|
</Typography.Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ValueText({ children }: { children: React.ReactNode }) {
|
||||||
|
return <Typography.Text style={{ fontSize: 16 }}>{children}</Typography.Text>;
|
||||||
|
}
|
|
@ -1,5 +1,4 @@
|
||||||
import { Button, Card, Flex, Typography } from "antd";
|
import { Button, Flex } from "antd";
|
||||||
import { MyContainer } from "../../../shared/components/MyContainer";
|
|
||||||
import { CheckOutlined } from "@ant-design/icons";
|
import { CheckOutlined } from "@ant-design/icons";
|
||||||
import HeaderBar from "../../../core/components/Header";
|
import HeaderBar from "../../../core/components/Header";
|
||||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
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 MyErrorResult from "shared/components/MyResult";
|
||||||
import MyEmpty from "shared/components/MyEmpty";
|
import MyEmpty from "shared/components/MyEmpty";
|
||||||
import { useGetLessonContentsQuery } from "core/services/lessons";
|
import { useGetLessonContentsQuery } from "core/services/lessons";
|
||||||
|
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||||
export type LessonContent = {
|
import { Converter } from "../converter";
|
||||||
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" ? (
|
|
||||||
<div style={{ fontWeight: "bold", fontSize: 24 }}>
|
|
||||||
{lessonContent.data}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Typography.Title
|
|
||||||
editable={{
|
|
||||||
triggerType: "text" as any,
|
|
||||||
onChange: (event) => onEdit?.(event),
|
|
||||||
}}
|
|
||||||
level={1}
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{lessonContent.data}
|
|
||||||
</Typography.Title>
|
|
||||||
);
|
|
||||||
case 1:
|
|
||||||
return mode === "view" ? (
|
|
||||||
<div style={{ fontSize: 16 }}>{lessonContent.data}</div>
|
|
||||||
) : (
|
|
||||||
<Typography.Text
|
|
||||||
editable={{
|
|
||||||
triggerType: "text" as any,
|
|
||||||
onChange: (event) => onEdit?.(event),
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{lessonContent.data}
|
|
||||||
</Typography.Text>
|
|
||||||
);
|
|
||||||
case 2:
|
|
||||||
return (
|
|
||||||
<img src={lessonContent.data} alt="img" style={{ width: "100%" }} />
|
|
||||||
);
|
|
||||||
case 3:
|
|
||||||
return (
|
|
||||||
<div style={{ fontWeight: "700", fontSize: 20 }}>
|
|
||||||
{lessonContent.data}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
case 4:
|
|
||||||
return <div style={{ fontSize: 14 }}>{lessonContent.data}</div>;
|
|
||||||
default:
|
|
||||||
return <div>Unknown type</div>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const LessonContents: React.FC = () => {
|
const LessonContents: React.FC = () => {
|
||||||
const { lessonId } = useParams();
|
const { lessonId } = useParams();
|
||||||
|
@ -130,7 +29,7 @@ const LessonContents: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data.map((lessonContent) => (
|
{data.map((lessonContent) => (
|
||||||
<div key={lessonContent.id} style={{ paddingBottom: 6 }}>
|
<div key={lessonContent.Id} style={{ paddingBottom: 6 }}>
|
||||||
<Converter mode="view" lessonContent={lessonContent} />
|
<Converter mode="view" lessonContent={lessonContent} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -150,14 +49,7 @@ export default function LessonPage() {
|
||||||
onEdit={() => navigate(`${location.pathname}/editor`)}
|
onEdit={() => navigate(`${location.pathname}/editor`)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MyContainer>
|
<MyMiddleCard>
|
||||||
<Flex justify="center">
|
|
||||||
<Card
|
|
||||||
style={{
|
|
||||||
width: 800,
|
|
||||||
maxWidth: 800,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LessonContents />
|
<LessonContents />
|
||||||
|
|
||||||
<Flex justify="right">
|
<Flex justify="right">
|
||||||
|
@ -165,9 +57,7 @@ export default function LessonPage() {
|
||||||
Finish lesson
|
Finish lesson
|
||||||
</Button>
|
</Button>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</MyMiddleCard>
|
||||||
</Flex>
|
|
||||||
</MyContainer>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,87 @@
|
||||||
import { DndContext, DragEndEvent, useDroppable } from "@dnd-kit/core";
|
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 SortableEditorItem from "./SortableEditorItem";
|
||||||
import React from "react";
|
|
||||||
import { LessonContent } from "../LessonPage";
|
|
||||||
import { store } from "core/store/store";
|
import { store } from "core/store/store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
restrictToVerticalAxis,
|
restrictToVerticalAxis,
|
||||||
restrictToWindowEdges,
|
restrictToWindowEdges,
|
||||||
} from "@dnd-kit/modifiers";
|
} 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 Droppable = ({ items }: { items: LessonContent[] }) => {
|
||||||
const droppableID = "editorComponentArea";
|
const droppableID = "editorComponentArea";
|
||||||
const { setNodeRef } = useDroppable({ id: droppableID });
|
const { setNodeRef } = useDroppable({ id: droppableID });
|
||||||
|
const currentLnId = useSelector(currentLessonId);
|
||||||
|
|
||||||
|
const [reqUpdateLessonContentPosition] =
|
||||||
|
useUpdateLessonContentPositionMutation();
|
||||||
|
|
||||||
const itemIDs = items.map((item) => item.id);
|
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 (
|
return (
|
||||||
<DndContext modifiers={[restrictToVerticalAxis, restrictToWindowEdges]} onDragEnd={handleDragEnd}>
|
<DndContext
|
||||||
|
modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
>
|
||||||
<SortableContext
|
<SortableContext
|
||||||
id={droppableID}
|
id={droppableID}
|
||||||
items={itemIDs}
|
items={itemIDs}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
<div ref={setNodeRef} >
|
<div ref={setNodeRef}>
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<SortableEditorItem key={`${droppableID}_${item.id}`} item={item} />
|
<SortableEditorItem key={`${droppableID}_${item.Id}`} item={item} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
/*
|
||||||
function handleDragEnd(event: DragEndEvent) {
|
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 activeId = event.active.id;
|
||||||
const overId = event.over.id;
|
const overId = event.over.id;
|
||||||
|
|
||||||
|
store.dispatch(onDragHandler({ activeId, overId }));
|
||||||
|
} */
|
||||||
store.dispatch(onDragHandler({activeId, overId}));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export default Droppable;
|
export default Droppable;
|
||||||
|
|
|
@ -1,54 +1,107 @@
|
||||||
import React from "react";
|
|
||||||
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { Converter, LessonContent } from "../LessonPage";
|
import {
|
||||||
import { HolderOutlined, DeleteOutlined } from "@ant-design/icons";
|
HolderOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
CameraOutlined,
|
||||||
|
FolderOpenOutlined,
|
||||||
|
} from "@ant-design/icons";
|
||||||
import { Flex } from "antd";
|
import { Flex } from "antd";
|
||||||
import { setLessonContent, deleteLessonContent } from "./lessonPageEditorSlice";
|
import {
|
||||||
import { useDispatch } from "react-redux";
|
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) =>
|
const animateLayoutChanges = (args: any) =>
|
||||||
args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true;
|
args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true;
|
||||||
|
|
||||||
const SortableEditorItem = (props: {item:LessonContent}) => {
|
const SortableEditorItem = (props: { item: LessonContent }) => {
|
||||||
const lnContent = props.item;
|
const lnContent = props.item;
|
||||||
const {
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
attributes,
|
useSortable({ id: lnContent.Id, animateLayoutChanges });
|
||||||
listeners,
|
|
||||||
setNodeRef,
|
|
||||||
transform,
|
|
||||||
transition
|
|
||||||
} = useSortable({ id: lnContent.id, animateLayoutChanges });
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const component = getComponentByType(lnContent.Type);
|
||||||
|
|
||||||
|
const [reqDeleteLessonContent] = useDeleteLessonContentMutation();
|
||||||
|
const currentLnId = useSelector(currentLessonId);
|
||||||
|
|
||||||
|
if (!component) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div
|
||||||
|
style={{
|
||||||
transform: CSS.Translate.toString(transform),
|
transform: CSS.Translate.toString(transform),
|
||||||
transition
|
transition,
|
||||||
}} ref={setNodeRef} {...attributes} >
|
}}
|
||||||
<Flex key={lnContent.id} >
|
ref={setNodeRef}
|
||||||
<HolderOutlined style={{paddingLeft:8,paddingRight:8, touchAction: "none", cursor: "move"}}{...listeners}/>
|
{...attributes}
|
||||||
|
>
|
||||||
|
<Flex key={lnContent.Id}>
|
||||||
|
<HolderOutlined
|
||||||
|
style={{
|
||||||
|
paddingLeft: 8,
|
||||||
|
paddingRight: 8,
|
||||||
|
touchAction: "none",
|
||||||
|
cursor: "move",
|
||||||
|
}}
|
||||||
|
{...listeners}
|
||||||
|
/>
|
||||||
<Converter
|
<Converter
|
||||||
mode="edititable"
|
mode="edititable"
|
||||||
lessonContent={lnContent}
|
lessonContent={lnContent}
|
||||||
onEdit={(data) =>
|
onEdit={(data) => {
|
||||||
|
console.log("edit", lnContent.Id, data);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
setLessonContent({
|
updateLessonContent({
|
||||||
id: lnContent.id,
|
id: lnContent.Id,
|
||||||
data: data,
|
data: data,
|
||||||
})
|
})
|
||||||
)
|
);
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<DeleteOutlined
|
|
||||||
onClick={() => {
|
|
||||||
console.log("delete", lnContent.id);
|
|
||||||
dispatch(deleteLessonContent(lnContent.id));
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Flex vertical justify="center">
|
||||||
|
{component.uploadImage ? (
|
||||||
|
<div className="EditorActionIcon">
|
||||||
|
<CameraOutlined />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{component.uploadFileTypes ? (
|
||||||
|
<div className="EditorActionIcon">
|
||||||
|
<FolderOpenOutlined className="EditorActionIcon" />{" "}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="EditorActionIcon">
|
||||||
|
<DeleteOutlined
|
||||||
|
className="EditorActionIcon"
|
||||||
|
onClick={() => {
|
||||||
|
console.log("delete", lnContent.Id);
|
||||||
|
dispatch(deleteLessonContent(lnContent.Id));
|
||||||
|
|
||||||
|
try {
|
||||||
|
reqDeleteLessonContent({
|
||||||
|
lessonId: currentLnId,
|
||||||
|
contentId: lnContent.Id,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,34 +2,43 @@ import { useNavigate, useParams } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
lessonContents,
|
lessonContents,
|
||||||
lessonThumbnail,
|
lessonThumbnail,
|
||||||
|
setCurrentLessonId,
|
||||||
setEditorActive,
|
setEditorActive,
|
||||||
|
setLessonContents,
|
||||||
|
setLessonState,
|
||||||
} from "./lessonPageEditorSlice";
|
} from "./lessonPageEditorSlice";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { LessonContent } from "../LessonPage";
|
|
||||||
import { Card, Flex } from "antd";
|
import { Card, Flex } from "antd";
|
||||||
import { Constants } from "../../../core/utils/utils";
|
import { Constants } from "core/utils/utils";
|
||||||
import HeaderBar from "../../../core/components/Header";
|
import HeaderBar from "core/components/Header";
|
||||||
import Droppable from "./Droppable";
|
import Droppable from "./Droppable";
|
||||||
import LessonPreviewCard from "../../../shared/components/MyLessonPreviewCard";
|
import LessonPreviewCard from "shared/components/MyLessonPreviewCard";
|
||||||
import {
|
import {
|
||||||
useGetLessonPreviewQuery,
|
useGetLessonContentsQuery,
|
||||||
|
useGetLessonSettingsQuery,
|
||||||
useUpdateLessonPreviewTitleMutation,
|
useUpdateLessonPreviewTitleMutation,
|
||||||
} from "core/services/lessons";
|
} from "core/services/lessons";
|
||||||
import MyErrorResult from "shared/components/MyResult";
|
import MyErrorResult from "shared/components/MyResult";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
import MyEmpty from "shared/components/MyEmpty";
|
||||||
|
|
||||||
const PreviewCard: React.FC = () => {
|
const PreviewCard: React.FC = () => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const { lessonId } = useParams();
|
const { lessonId } = useParams();
|
||||||
|
|
||||||
const { data, error, isLoading, refetch } = useGetLessonPreviewQuery(
|
const { data, error, isLoading, refetch } = useGetLessonSettingsQuery(
|
||||||
lessonId as string,
|
lessonId as string,
|
||||||
{
|
{
|
||||||
refetchOnMountOrArgChange: true,
|
refetchOnMountOrArgChange: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const [updateLessonPreviewTitle, {}] = useUpdateLessonPreviewTitleMutation();
|
const [updateLessonPreviewTitle] = useUpdateLessonPreviewTitleMutation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data?.State) dispatch(setLessonState(data.State));
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
if (error) return <MyErrorResult />;
|
if (error) return <MyErrorResult />;
|
||||||
|
|
||||||
|
@ -38,7 +47,7 @@ const PreviewCard: React.FC = () => {
|
||||||
mode="editable"
|
mode="editable"
|
||||||
lessonId={lessonId as string}
|
lessonId={lessonId as string}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
lessonPreview={{
|
lessonSettings={{
|
||||||
Title: data?.Title || "",
|
Title: data?.Title || "",
|
||||||
ThumbnailUrl: data?.ThumbnailUrl || "",
|
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 <MyErrorResult />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card loading={isLoading}>
|
||||||
|
<Flex vertical gap={16}>
|
||||||
|
{!lnContents || lnContents.length == 0 ? (
|
||||||
|
<MyEmpty />
|
||||||
|
) : (
|
||||||
|
<Droppable items={lnContents} />
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function LessonPageEditor() {
|
export default function LessonPageEditor() {
|
||||||
const { lessonId } = useParams();
|
const { lessonId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -71,6 +114,7 @@ export default function LessonPageEditor() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
dispatch(setEditorActive(true));
|
dispatch(setEditorActive(true));
|
||||||
|
dispatch(setCurrentLessonId(lessonId as string));
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
dispatch(setEditorActive(false));
|
dispatch(setEditorActive(false));
|
||||||
|
@ -104,22 +148,9 @@ export default function LessonPageEditor() {
|
||||||
>
|
>
|
||||||
<PreviewCard />
|
<PreviewCard />
|
||||||
|
|
||||||
<Card>
|
<LessonContentComponents />
|
||||||
<Flex vertical gap={16}>
|
|
||||||
<Droppable items={lnContents} />
|
|
||||||
</Flex>
|
|
||||||
</Card>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StandardEditorCompontent(type: number): LessonContent {
|
|
||||||
return {
|
|
||||||
id: Math.floor(Math.random() * 10000).toString(),
|
|
||||||
position: 1,
|
|
||||||
type: type,
|
|
||||||
data: "Some data",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,76 +1,89 @@
|
||||||
import { createSlice } from "@reduxjs/toolkit";
|
import { createSlice } from "@reduxjs/toolkit";
|
||||||
import { LessonContent } from "../LessonPage";
|
import { LessonContent, LessonState } from "core/types/lesson";
|
||||||
import { StandardEditorCompontent } from ".";
|
|
||||||
|
interface AddLessonContentAction {
|
||||||
|
type: string;
|
||||||
|
payload: LessonContent;
|
||||||
|
}
|
||||||
|
|
||||||
export const lessonPageEditorSlice = createSlice({
|
export const lessonPageEditorSlice = createSlice({
|
||||||
name: "lessonPageEditor",
|
name: "lessonPageEditor",
|
||||||
initialState: {
|
initialState: {
|
||||||
editorActive: false,
|
editorActive: false,
|
||||||
|
currentLessonId: "", // required in sideMenu because has no access to useParams
|
||||||
lessonThumbnail: {
|
lessonThumbnail: {
|
||||||
img: "",
|
img: "",
|
||||||
title: "Test",
|
title: "Test",
|
||||||
},
|
},
|
||||||
lessonContents: [] as LessonContent[],
|
lessonContents: [] as LessonContent[],
|
||||||
|
lessonState: LessonState.Draft,
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setEditorActive: (state, action) => {
|
setEditorActive: (state, action) => {
|
||||||
state.editorActive = action.payload;
|
state.editorActive = action.payload;
|
||||||
},
|
},
|
||||||
addLessonContent: (state, action) => {
|
setCurrentLessonId: (state, action) => {
|
||||||
state.lessonContents.push(StandardEditorCompontent(action.payload));
|
state.currentLessonId = action.payload;
|
||||||
|
},
|
||||||
|
addLessonContent: (state, action: AddLessonContentAction) => {
|
||||||
|
state.lessonContents.push(action.payload);
|
||||||
},
|
},
|
||||||
deleteLessonContent: (state, action) => {
|
deleteLessonContent: (state, action) => {
|
||||||
state.lessonContents = state.lessonContents.filter(
|
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(
|
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) => {
|
setLessonThumbnailTitle: (state, action) => {
|
||||||
state.lessonThumbnail.title = action.payload;
|
state.lessonThumbnail.title = action.payload;
|
||||||
},
|
},
|
||||||
onDragHandler: (state, action) => {
|
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(
|
state.lessonContents.splice(
|
||||||
newIndex,
|
action.payload.newIndex,
|
||||||
0,
|
0,
|
||||||
state.lessonContents.splice(oldIndex, 1)[0]
|
state.lessonContents.splice(action.payload.oldIndex, 1)[0]
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
setLessonState: (state, action) => {
|
||||||
|
state.lessonState = action.payload;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
editorActive: (state) => state.editorActive,
|
editorActive: (state) => state.editorActive,
|
||||||
|
currentLessonId: (state) => state.currentLessonId,
|
||||||
lessonContents: (state) => state.lessonContents,
|
lessonContents: (state) => state.lessonContents,
|
||||||
lessonThumbnail: (state) => state.lessonThumbnail,
|
lessonThumbnail: (state) => state.lessonThumbnail,
|
||||||
|
lessonState: (state) => state.lessonState,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
setEditorActive,
|
setEditorActive,
|
||||||
|
setCurrentLessonId,
|
||||||
addLessonContent,
|
addLessonContent,
|
||||||
deleteLessonContent,
|
deleteLessonContent,
|
||||||
setLessonContent,
|
setLessonContents,
|
||||||
|
updateLessonContent,
|
||||||
setLessonThumbnailTitle,
|
setLessonThumbnailTitle,
|
||||||
onDragHandler,
|
onDragHandler,
|
||||||
|
setLessonState,
|
||||||
} = lessonPageEditorSlice.actions;
|
} = lessonPageEditorSlice.actions;
|
||||||
|
|
||||||
export const { editorActive, lessonContents, lessonThumbnail } =
|
export const {
|
||||||
lessonPageEditorSlice.selectors;
|
editorActive,
|
||||||
|
currentLessonId,
|
||||||
|
lessonContents,
|
||||||
|
lessonThumbnail,
|
||||||
|
lessonState,
|
||||||
|
} = lessonPageEditorSlice.selectors;
|
||||||
|
|
|
@ -8,3 +8,8 @@
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.EditorActionIcon {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,9 @@ export type Component = {
|
||||||
name: string;
|
name: string;
|
||||||
thumbnail?: string;
|
thumbnail?: string;
|
||||||
invertThumbnailAtDarkmode?: boolean;
|
invertThumbnailAtDarkmode?: boolean;
|
||||||
|
uploadFileTypes?: string[];
|
||||||
|
uploadImage?: boolean;
|
||||||
|
defaultData?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const componentsGroups: ComponentGroup[] = [
|
const componentsGroups: ComponentGroup[] = [
|
||||||
|
@ -21,12 +24,14 @@ const componentsGroups: ComponentGroup[] = [
|
||||||
name: "Header",
|
name: "Header",
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_header.svg",
|
thumbnail: "/editor/thumbnails/component_thumbnail_header.svg",
|
||||||
invertThumbnailAtDarkmode: true,
|
invertThumbnailAtDarkmode: true,
|
||||||
|
defaultData: "Header",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 1,
|
type: 1,
|
||||||
name: "Text",
|
name: "Text",
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_text.svg",
|
thumbnail: "/editor/thumbnails/component_thumbnail_text.svg",
|
||||||
invertThumbnailAtDarkmode: true,
|
invertThumbnailAtDarkmode: true,
|
||||||
|
defaultData: "Text",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -34,18 +39,20 @@ const componentsGroups: ComponentGroup[] = [
|
||||||
category: "Media",
|
category: "Media",
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: 0,
|
type: 2,
|
||||||
name: "Image",
|
name: "Image",
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_image.png",
|
thumbnail: "/editor/thumbnails/component_thumbnail_image.png",
|
||||||
|
uploadImage: true,
|
||||||
|
uploadFileTypes: ["image/*"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 1,
|
type: 3,
|
||||||
name: "YouTube",
|
name: "YouTube",
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
||||||
invertThumbnailAtDarkmode: true,
|
invertThumbnailAtDarkmode: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 1,
|
type: 4,
|
||||||
name: "Video",
|
name: "Video",
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
||||||
invertThumbnailAtDarkmode: true,
|
invertThumbnailAtDarkmode: true,
|
||||||
|
@ -56,7 +63,7 @@ const componentsGroups: ComponentGroup[] = [
|
||||||
category: "HTML",
|
category: "HTML",
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: 0,
|
type: 5,
|
||||||
name: "Iframe",
|
name: "Iframe",
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_iframe.svg",
|
thumbnail: "/editor/thumbnails/component_thumbnail_iframe.svg",
|
||||||
invertThumbnailAtDarkmode: true,
|
invertThumbnailAtDarkmode: true,
|
||||||
|
@ -67,7 +74,7 @@ const componentsGroups: ComponentGroup[] = [
|
||||||
category: "Special",
|
category: "Special",
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: 0,
|
type: 6,
|
||||||
name: "Banner",
|
name: "Banner",
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_banner.png",
|
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 };
|
export { componentsGroups };
|
||||||
|
|
|
@ -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 | NodeJS.Timeout>(null);
|
||||||
|
|
||||||
|
switch (lessonContent.Type) {
|
||||||
|
case getTypeByName("Header"):
|
||||||
|
if (mode === "view") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{ fontWeight: "bold", fontSize: 24, wordBreak: "break-all" }}
|
||||||
|
>
|
||||||
|
{lessonContent.Data}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Typography.Title
|
||||||
|
editable={{
|
||||||
|
triggerType: "text" as any,
|
||||||
|
onChange: (event) => {
|
||||||
|
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}
|
||||||
|
</Typography.Title>
|
||||||
|
);
|
||||||
|
case getTypeByName("Text"):
|
||||||
|
if (mode === "view") {
|
||||||
|
return (
|
||||||
|
<div style={{ fontSize: 16, wordBreak: "break-all" }}>
|
||||||
|
{lessonContent.Data}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input.TextArea
|
||||||
|
variant="borderless"
|
||||||
|
placeholder="Input text here"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
value={lessonContent.Data}
|
||||||
|
onChange={(event) => {
|
||||||
|
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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
height: 120,
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "#EBEBEB",
|
||||||
|
marginRight: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No image provided
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lessonContent.Data === "") {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
height: 120,
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "#EBEBEB",
|
||||||
|
margin: "12px 12px 12px 0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span>Choose image from</span>
|
||||||
|
|
||||||
|
<MyUpload>
|
||||||
|
<Button type="link">Gallery</Button>
|
||||||
|
</MyUpload>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<img src={lessonContent.Data} alt="img" style={{ width: "100%" }} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case getTypeByName("YouTube"):
|
||||||
|
return (
|
||||||
|
<div style={{ fontWeight: "700", fontSize: 20, width: "100%" }}>
|
||||||
|
{lessonContent.Data}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
case getTypeByName("Video"):
|
||||||
|
return <div style={{ fontSize: 14, width: "100%" }}>Not implemented</div>;
|
||||||
|
case getTypeByName("Iframe"):
|
||||||
|
return <div style={{ fontSize: 14, width: "100%" }}>Not implemented</div>;
|
||||||
|
case getTypeByName("Banner"):
|
||||||
|
return <div style={{ fontSize: 14, width: "100%" }}>Not implemented</div>;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <div>Unknown type</div>;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Button, Flex, Segmented } from "antd";
|
import { Button, Divider, Flex, Segmented } from "antd";
|
||||||
import MyBanner from "../../shared/components/MyBanner";
|
import MyBanner from "shared/components/MyBanner";
|
||||||
import { MyContainer } from "../../shared/components/MyContainer";
|
import { MyContainer } from "shared/components/MyContainer";
|
||||||
import {
|
import {
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
BarsOutlined,
|
BarsOutlined,
|
||||||
|
@ -8,7 +8,7 @@ import {
|
||||||
} from "@ant-design/icons";
|
} from "@ant-design/icons";
|
||||||
import Search, { SearchProps } from "antd/es/input/Search";
|
import Search, { SearchProps } from "antd/es/input/Search";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import HeaderBar from "../../core/components/Header";
|
import HeaderBar from "core/components/Header";
|
||||||
import {
|
import {
|
||||||
useCreateLessonMutation,
|
useCreateLessonMutation,
|
||||||
useGetLessonsQuery,
|
useGetLessonsQuery,
|
||||||
|
@ -17,7 +17,7 @@ import MySpin from "shared/components/MySpin";
|
||||||
import { Constants } from "core/utils/utils";
|
import { Constants } from "core/utils/utils";
|
||||||
import MyEmpty from "shared/components/MyEmpty";
|
import MyEmpty from "shared/components/MyEmpty";
|
||||||
import MyErrorResult from "shared/components/MyResult";
|
import MyErrorResult from "shared/components/MyResult";
|
||||||
import LessonPreviewCard from "../../shared/components/MyLessonPreviewCard";
|
import LessonPreviewCard from "shared/components/MyLessonPreviewCard";
|
||||||
|
|
||||||
const CreateLessonButton: React.FC = () => {
|
const CreateLessonButton: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
@ -61,21 +61,47 @@ const LessonList: React.FC = () => {
|
||||||
|
|
||||||
if (!data || data.length === 0) return <MyEmpty />;
|
if (!data || data.length === 0) return <MyEmpty />;
|
||||||
|
|
||||||
|
const publishedItems = data.filter(item => item.State === 1);
|
||||||
|
const unpublishedItems = data.filter(item => item.State === 2);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data.map((item, index) => (
|
{publishedItems.length > 0 && (
|
||||||
|
<>
|
||||||
|
{publishedItems.map((item, index) => (
|
||||||
<LessonPreviewCard
|
<LessonPreviewCard
|
||||||
key={index}
|
key={index}
|
||||||
mode="view"
|
mode="view"
|
||||||
lessonId={item.Id}
|
lessonId={item.Id}
|
||||||
loading={false}
|
loading={false}
|
||||||
lessonPreview={{
|
lessonSettings={{
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
ThumbnailUrl: item.ThumbnailUrl,
|
ThumbnailUrl: item.ThumbnailUrl,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{unpublishedItems.length > 0 && (
|
||||||
|
<>
|
||||||
|
<Divider orientation="left" style={{marginBottom: 0}}>Unpublished</Divider>
|
||||||
|
|
||||||
|
{unpublishedItems.map((item, index) => (
|
||||||
|
<LessonPreviewCard
|
||||||
|
key={index}
|
||||||
|
mode="view"
|
||||||
|
lessonId={item.Id}
|
||||||
|
loading={false}
|
||||||
|
lessonSettings={{
|
||||||
|
Title: item.Title,
|
||||||
|
ThumbnailUrl: item.ThumbnailUrl,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,137 @@
|
||||||
|
import { Button, Card, Divider, Flex, Form, Input, Typography } from "antd";
|
||||||
import HeaderBar from "../../core/components/Header";
|
import HeaderBar from "../../core/components/Header";
|
||||||
import MyBanner from "../../shared/components/MyBanner";
|
import MyBanner from "../../shared/components/MyBanner";
|
||||||
import { MyContainer } from "../../shared/components/MyContainer";
|
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() {
|
export default function Settings() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={
|
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||||
<HeaderBar />
|
|
||||||
} />
|
|
||||||
|
|
||||||
<MyContainer>
|
<MyContainer>
|
||||||
<h1>Settings</h1>
|
<Flex vertical gap={16} style={{ paddingBottom: 16 }}>
|
||||||
|
<Card
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex vertical gap={2}>
|
||||||
|
<Form>
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||||
|
Branding
|
||||||
|
</Typography.Title>
|
||||||
|
<Button
|
||||||
|
icon={<SaveOutlined />}
|
||||||
|
type="text"
|
||||||
|
shape="circle"
|
||||||
|
size="large"
|
||||||
|
htmlType="submit"
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Divider style={{ margin: 0, padding: 0 }} />
|
||||||
|
|
||||||
|
<Flex gap={32}>
|
||||||
|
<Flex vertical>
|
||||||
|
<Typography.Text style={{ fontSize: 16 }}>
|
||||||
|
Primary Color
|
||||||
|
</Typography.Text>
|
||||||
|
<Form.Item name="primaryColor">
|
||||||
|
<ColorPicker
|
||||||
|
defaultValue="#1677ff"
|
||||||
|
size="small"
|
||||||
|
showText
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Flex>
|
||||||
|
<Flex vertical>
|
||||||
|
<Typography.Text style={{ fontSize: 16 }}>
|
||||||
|
Company name
|
||||||
|
</Typography.Text>
|
||||||
|
<Form.Item name="companyName">
|
||||||
|
<Input defaultValue="Jannex" />
|
||||||
|
</Form.Item>
|
||||||
|
</Flex>
|
||||||
|
<Flex vertical>
|
||||||
|
<Typography.Text style={{ fontSize: 16 }}>
|
||||||
|
Subdomain
|
||||||
|
</Typography.Text>
|
||||||
|
<Form.Item name="subdomain">
|
||||||
|
<Input addonBefore="https://" addonAfter=". jannex . de" defaultValue="mysite" />
|
||||||
|
</Form.Item>
|
||||||
|
</Flex>
|
||||||
|
<Flex vertical>
|
||||||
|
<Typography.Text style={{ fontSize: 16 }}>
|
||||||
|
Logo
|
||||||
|
</Typography.Text>
|
||||||
|
<MyUpload
|
||||||
|
action={`/changeCompanyLogo`}
|
||||||
|
onChange={(info) => {
|
||||||
|
if (info.file.status === "done") {
|
||||||
|
//onThumbnailChanged?.();
|
||||||
|
console.log("done");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
imgCropProps={{
|
||||||
|
aspect: 1 / 1,
|
||||||
|
children: <></>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
||||||
|
alt="Company Logo"
|
||||||
|
style={{
|
||||||
|
width: 128,
|
||||||
|
maxHeight: 128,
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
border: "1px solid #ddd",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MyUpload>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<Flex vertical>
|
||||||
|
<Typography.Text style={{ fontSize: 16 }}>
|
||||||
|
Thumbnail
|
||||||
|
</Typography.Text>
|
||||||
|
<MyUpload
|
||||||
|
action={`/changeCompanyLogo`}
|
||||||
|
onChange={(info) => {
|
||||||
|
if (info.file.status === "done") {
|
||||||
|
//onThumbnailChanged?.();
|
||||||
|
console.log("done");
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
imgCropProps={{
|
||||||
|
aspect: 22 / 9,
|
||||||
|
children: <></>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
||||||
|
alt="Thumbnail"
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: 228,
|
||||||
|
objectFit: "cover",
|
||||||
|
padding: 4,
|
||||||
|
borderRadius: 4,
|
||||||
|
border: "1px solid #ddd",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MyUpload>
|
||||||
|
</Flex>
|
||||||
|
</Form>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
</MyContainer>
|
</MyContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,83 @@
|
||||||
|
import { PlusOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Card, Checkbox, Flex, Form, Input } from "antd";
|
||||||
|
import HeaderBar from "core/components/Header";
|
||||||
|
import { Constants } from "core/utils/utils";
|
||||||
|
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||||
|
|
||||||
|
type FieldType = {
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
email: string;
|
||||||
|
changePasswordOnFirstLogin: boolean;
|
||||||
|
password?: string;
|
||||||
|
sendInvitationEmail: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TeamCreateUser() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HeaderBar
|
||||||
|
theme="light"
|
||||||
|
backTo={Constants.ROUTE_PATHS.ORGANIZATION_TEAM}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MyMiddleCard title="Create User">
|
||||||
|
<Form layout="vertical" requiredMark={false}>
|
||||||
|
<Form.Item<FieldType>
|
||||||
|
label="First Name"
|
||||||
|
name="firstName"
|
||||||
|
rules={[{ required: true, message: "Please input first name!" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="First Name" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<FieldType>
|
||||||
|
label="Last Name"
|
||||||
|
name="lastName"
|
||||||
|
rules={[{ required: true, message: "Please input last name!" }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Last Name" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<FieldType>
|
||||||
|
label="Email"
|
||||||
|
name="email"
|
||||||
|
rules={[
|
||||||
|
{ required: true, message: "Please input email!", type: "email" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input placeholder="Email" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<FieldType>
|
||||||
|
name="changePasswordOnFirstLogin"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Checkbox>Change Password on First Login</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<FieldType>
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
rules={[{ required: true, message: "Please input password!" }]}
|
||||||
|
>
|
||||||
|
<Input.Password placeholder="Password" />
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item<FieldType>
|
||||||
|
name="sendInvitationEmail"
|
||||||
|
valuePropName="checked"
|
||||||
|
>
|
||||||
|
<Checkbox>Send an invitation email to the user</Checkbox>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Flex justify="end">
|
||||||
|
<Button type="primary" icon={<PlusOutlined />}>
|
||||||
|
Create User
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
</Form>
|
||||||
|
</MyMiddleCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,16 +1,101 @@
|
||||||
|
import MyTable from "shared/components/MyTable";
|
||||||
import HeaderBar from "../../core/components/Header";
|
import HeaderBar from "../../core/components/Header";
|
||||||
import MyBanner from "../../shared/components/MyBanner";
|
import MyBanner from "../../shared/components/MyBanner";
|
||||||
import { MyContainer } from "../../shared/components/MyContainer";
|
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 <MyErrorResult />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MyTable loading={isLoading} columns={getTableContent()} dataSource={getTableItems()} pagination={false} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default function Team() {
|
export default function Team() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MyBanner title="Team" subtitle="MANAGE" headerBar={
|
<MyBanner title="Team" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||||
<HeaderBar />
|
|
||||||
} />
|
|
||||||
|
|
||||||
<MyContainer>
|
<MyContainer
|
||||||
<h1>Team</h1>
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex justify="end">
|
||||||
|
<Link to={Constants.ROUTE_PATHS.ORGANIZATION_TEAM_CREATE_USER}>
|
||||||
|
<Button icon={<UserAddOutlined />}>Invite new member</Button>
|
||||||
|
</Link>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
|
<TeamList />
|
||||||
</MyContainer>
|
</MyContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { Content } from "antd/es/layout/layout";
|
import { Content } from "antd/es/layout/layout";
|
||||||
|
|
||||||
export function MyContainer({ children }: { children: React.ReactNode }) {
|
export function MyContainer({
|
||||||
return <Content style={{ padding: 12 }}>{children}</Content>;
|
children,
|
||||||
|
style = {},
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}) {
|
||||||
|
return <Content style={{ padding: 12, ...style }}>{children}</Content>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MyCenteredContainer({
|
export function MyCenteredContainer({
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
import { CommentOutlined } from "@ant-design/icons";
|
import { CommentOutlined } from "@ant-design/icons";
|
||||||
import { Card, Flex, Typography } from "antd";
|
import { Card, Flex, Typography } from "antd";
|
||||||
import { LessonPreview } from "core/types/lesson";
|
|
||||||
import { Constants, getImageUrl } from "core/utils/utils";
|
import { Constants, getImageUrl } from "core/utils/utils";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import MyUpload from "shared/components/MyUpload";
|
import MyUpload from "shared/components/MyUpload";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
import { LessonSettings } from "core/types/lesson";
|
||||||
|
|
||||||
export default function MyLessonPreviewCard({
|
export default function MyLessonPreviewCard({
|
||||||
mode = "view",
|
mode = "view",
|
||||||
lessonId,
|
lessonId,
|
||||||
loading = false,
|
loading = false,
|
||||||
lessonPreview,
|
lessonSettings,
|
||||||
onEditTitle,
|
onEditTitle,
|
||||||
onThumbnailChanged,
|
onThumbnailChanged,
|
||||||
}: {
|
}: {
|
||||||
mode: "view" | "editable";
|
mode: "view" | "editable";
|
||||||
lessonId: string;
|
lessonId: string;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
lessonPreview: LessonPreview;
|
lessonSettings: LessonSettings;
|
||||||
onEditTitle?: (newTitle: string) => void;
|
onEditTitle?: (newTitle: string) => void;
|
||||||
onThumbnailChanged?: () => void;
|
onThumbnailChanged?: () => void;
|
||||||
}) {
|
}) {
|
||||||
|
@ -61,9 +61,9 @@ export default function MyLessonPreviewCard({
|
||||||
<UploadWrapper>
|
<UploadWrapper>
|
||||||
<img
|
<img
|
||||||
src={
|
src={
|
||||||
lessonPreview.ThumbnailUrl === ""
|
lessonSettings.ThumbnailUrl === ""
|
||||||
? `${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`
|
? `${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`
|
||||||
: getImageUrl(lessonPreview.ThumbnailUrl)
|
: getImageUrl(lessonSettings.ThumbnailUrl)
|
||||||
}
|
}
|
||||||
alt="lesson thumbnail"
|
alt="lesson thumbnail"
|
||||||
className={styles.img}
|
className={styles.img}
|
||||||
|
@ -73,7 +73,7 @@ export default function MyLessonPreviewCard({
|
||||||
<Flex vertical justify="center" style={{ width: "100%" }}>
|
<Flex vertical justify="center" style={{ width: "100%" }}>
|
||||||
{mode === "view" ? (
|
{mode === "view" ? (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.title}>{lessonPreview.Title}</div>
|
<div className={styles.title}>{lessonSettings.Title}</div>
|
||||||
<CommentOutlined /> 12 comments
|
<CommentOutlined /> 12 comments
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -88,7 +88,7 @@ export default function MyLessonPreviewCard({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{lessonPreview.Title}
|
{lessonSettings.Title}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { Card, CardProps, Flex } from "antd";
|
||||||
|
import { MyContainer } from "../MyContainer";
|
||||||
|
|
||||||
|
interface MyMiddleCardProps extends CardProps {}
|
||||||
|
|
||||||
|
const MyMiddleCard: React.FC<MyMiddleCardProps> = ({ children,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<MyContainer>
|
||||||
|
<Flex justify="center">
|
||||||
|
<Card
|
||||||
|
style={{
|
||||||
|
width: 800,
|
||||||
|
maxWidth: 800,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Card>
|
||||||
|
</Flex>
|
||||||
|
</MyContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyMiddleCard;
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Table, TableProps } from "antd";
|
||||||
|
|
||||||
|
interface MyTableProps extends TableProps<any> {}
|
||||||
|
|
||||||
|
const MyTable: React.FC<MyTableProps> = (props) => {
|
||||||
|
return <Table {...props} scroll={{ x: "max-content" }} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MyTable;
|
|
@ -49,6 +49,7 @@ export default function MyUpload({
|
||||||
action={`${Constants.API_ADDRESS}${action}`}
|
action={`${Constants.API_ADDRESS}${action}`}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
beforeUpload={beforeUpload}
|
beforeUpload={beforeUpload}
|
||||||
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Upload>
|
</Upload>
|
||||||
|
|
Loading…
Reference in New Issue