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 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() {
|
|||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.ORGANIZATION_TEAM_CREATE_USER}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<TeamCreateUser />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.ORGANIZATION_ROLES}
|
||||
element={
|
||||
|
@ -60,7 +71,7 @@ export default function AppRoutes() {
|
|||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.ORGANIZATION_SETTINGS}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
|
@ -69,6 +80,15 @@ export default function AppRoutes() {
|
|||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.ACCOUNT_SETTINGS}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<AccountSettings />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Constants.ROUTE_PATHS.WHATS_NEW}
|
||||
element={
|
||||
|
|
|
@ -3,7 +3,7 @@ import { DraggableCreateComponent } from "../SideMenu";
|
|||
import { componentsGroups } from "features/Lessons/components";
|
||||
|
||||
function MyDndContext({ children }: { children: React.ReactNode }) {
|
||||
return <DndContext collisionDetection={closestCenter}>{children}
|
||||
return <DndContext >{children}
|
||||
</DndContext>;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<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 (
|
||||
<MyContainer>
|
||||
<Search
|
||||
placeholder="What would you like to insert?"
|
||||
style={{ paddingBottom: 16 }}
|
||||
/>
|
||||
<Flex justify="space-between" vertical style={{ height: "100%" }}>
|
||||
<MyContainer>
|
||||
<Search
|
||||
placeholder="What would you like to insert?"
|
||||
style={{ paddingBottom: 16 }}
|
||||
/>
|
||||
|
||||
<DndContext
|
||||
onDragStart={(event) => {
|
||||
console.log("drag start", event.active.id);
|
||||
<DndContext
|
||||
onDragStart={(event) => {
|
||||
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) => (
|
||||
<div key={i}>
|
||||
<span>{group.category}</span>
|
||||
setIsDragging(event.active.id.toString());
|
||||
}}
|
||||
onDragEnd={(evnet) => {
|
||||
console.log("drag end", evnet.active.id);
|
||||
setIsDragging(null);
|
||||
}}
|
||||
>
|
||||
{componentsGroups.map((group, i) => (
|
||||
<div key={i}>
|
||||
<span>{group.category}</span>
|
||||
|
||||
<Flex gap={16} wrap style={{ paddingTop: 16 }}>
|
||||
{group.components.map((component, i) => (
|
||||
<DraggableCreateComponent key={i} component={component} />
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
{createPortal(
|
||||
<DragOverlay>
|
||||
{isDragging
|
||||
? (() => {
|
||||
const comp = componentsGroups
|
||||
.flatMap((group) => group.components)
|
||||
.find((comp) => "draggable_" + comp.name === isDragging);
|
||||
console.log("dragging", comp);
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
<Flex gap={16} wrap style={{ paddingTop: 16 }}>
|
||||
{group.components.map((component, i) => (
|
||||
<DraggableCreateComponent key={i} component={component} />
|
||||
))}
|
||||
</Flex>
|
||||
</div>
|
||||
))}
|
||||
{createPortal(
|
||||
<DragOverlay>
|
||||
{isDragging
|
||||
? (() => {
|
||||
const comp = componentsGroups
|
||||
.flatMap((group) => group.components)
|
||||
.find((comp) => "draggable_" + comp.name === isDragging);
|
||||
console.log("dragging", comp);
|
||||
if (!comp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{}}>
|
||||
<CreateComponent component={comp} />
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
: null}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
</DndContext>
|
||||
</MyContainer>
|
||||
return (
|
||||
<div style={{}}>
|
||||
<CreateComponent component={comp} />
|
||||
</div>
|
||||
);
|
||||
})()
|
||||
: null}
|
||||
</DragOverlay>,
|
||||
document.body
|
||||
)}
|
||||
</DndContext>
|
||||
</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 { attributes, listeners, setNodeRef, transform, isDragging, active } =
|
||||
/*const { attributes, listeners, setNodeRef, transform, isDragging, active } =
|
||||
useDraggable({
|
||||
id: "draggable_" + component.name,
|
||||
});
|
||||
});*/
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
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,
|
||||
}}
|
||||
onClick={() => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CreateComponent component={component} />
|
||||
</div>
|
||||
|
@ -307,6 +354,10 @@ function CreateComponent({ component }: { component: Component }) {
|
|||
const dispatch = useDispatch();
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
|
||||
const currentLnId = useSelector(currentLessonId);
|
||||
|
||||
const [reqAddLessonContent] = useAddLessonContentMutation();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
vertical
|
||||
|
@ -318,9 +369,30 @@ function CreateComponent({ component }: { component: Component }) {
|
|||
width: 80,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
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 ? (
|
||||
|
|
|
@ -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<LessonPreview, string>({
|
||||
getLessonSettings: builder.query<LessonSettings, string>({
|
||||
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;
|
||||
|
|
|
@ -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 { 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
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
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",
|
||||
|
|
|
@ -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 { 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" ? (
|
||||
<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>;
|
||||
}
|
||||
}
|
||||
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) => (
|
||||
<div key={lessonContent.id} style={{ paddingBottom: 6 }}>
|
||||
<div key={lessonContent.Id} style={{ paddingBottom: 6 }}>
|
||||
<Converter mode="view" lessonContent={lessonContent} />
|
||||
</div>
|
||||
))}
|
||||
|
@ -150,24 +49,15 @@ export default function LessonPage() {
|
|||
onEdit={() => navigate(`${location.pathname}/editor`)}
|
||||
/>
|
||||
|
||||
<MyContainer>
|
||||
<Flex justify="center">
|
||||
<Card
|
||||
style={{
|
||||
width: 800,
|
||||
maxWidth: 800,
|
||||
}}
|
||||
>
|
||||
<LessonContents />
|
||||
<MyMiddleCard>
|
||||
<LessonContents />
|
||||
|
||||
<Flex justify="right">
|
||||
<Button type="primary" icon={<CheckOutlined />}>
|
||||
Finish lesson
|
||||
</Button>
|
||||
</Flex>
|
||||
</Card>
|
||||
<Flex justify="right">
|
||||
<Button type="primary" icon={<CheckOutlined />}>
|
||||
Finish lesson
|
||||
</Button>
|
||||
</Flex>
|
||||
</MyContainer>
|
||||
</MyMiddleCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 (
|
||||
<DndContext modifiers={[restrictToVerticalAxis, restrictToWindowEdges]} onDragEnd={handleDragEnd}>
|
||||
<DndContext
|
||||
modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
id={droppableID}
|
||||
items={itemIDs}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div ref={setNodeRef} >
|
||||
<div ref={setNodeRef}>
|
||||
{items.map((item) => (
|
||||
<SortableEditorItem key={`${droppableID}_${item.id}`} item={item} />
|
||||
<SortableEditorItem key={`${droppableID}_${item.Id}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
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;
|
||||
|
|
|
@ -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 (
|
||||
<div style={{
|
||||
<div
|
||||
style={{
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition
|
||||
}} ref={setNodeRef} {...attributes} >
|
||||
<Flex key={lnContent.id} >
|
||||
<HolderOutlined style={{paddingLeft:8,paddingRight:8, touchAction: "none", cursor: "move"}}{...listeners}/>
|
||||
<Converter
|
||||
mode="edititable"
|
||||
lessonContent={lnContent}
|
||||
onEdit={(data) =>
|
||||
dispatch(
|
||||
setLessonContent({
|
||||
id: lnContent.id,
|
||||
data: data,
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
transition,
|
||||
}}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
>
|
||||
<Flex key={lnContent.Id}>
|
||||
<HolderOutlined
|
||||
style={{
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
touchAction: "none",
|
||||
cursor: "move",
|
||||
}}
|
||||
{...listeners}
|
||||
/>
|
||||
<Converter
|
||||
mode="edititable"
|
||||
lessonContent={lnContent}
|
||||
onEdit={(data) => {
|
||||
console.log("edit", lnContent.Id, data);
|
||||
|
||||
dispatch(
|
||||
updateLessonContent({
|
||||
id: lnContent.Id,
|
||||
data: data,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<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));
|
||||
console.log("delete", lnContent.Id);
|
||||
dispatch(deleteLessonContent(lnContent.Id));
|
||||
|
||||
try {
|
||||
reqDeleteLessonContent({
|
||||
lessonId: currentLnId,
|
||||
contentId: lnContent.Id,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 <MyErrorResult />;
|
||||
|
||||
|
@ -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 <MyErrorResult />;
|
||||
|
||||
return (
|
||||
<Card loading={isLoading}>
|
||||
<Flex vertical gap={16}>
|
||||
{!lnContents || lnContents.length == 0 ? (
|
||||
<MyEmpty />
|
||||
) : (
|
||||
<Droppable items={lnContents} />
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
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() {
|
|||
>
|
||||
<PreviewCard />
|
||||
|
||||
<Card>
|
||||
<Flex vertical gap={16}>
|
||||
<Droppable items={lnContents} />
|
||||
</Flex>
|
||||
</Card>
|
||||
<LessonContentComponents />
|
||||
</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 { 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;
|
||||
|
|
|
@ -8,3 +8,8 @@
|
|||
max-width: 800px;
|
||||
}
|
||||
}
|
||||
|
||||
.EditorActionIcon {
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
}
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 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 <MyEmpty />;
|
||||
|
||||
const publishedItems = data.filter(item => item.State === 1);
|
||||
const unpublishedItems = data.filter(item => item.State === 2);
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.map((item, index) => (
|
||||
<LessonPreviewCard
|
||||
key={index}
|
||||
mode="view"
|
||||
lessonId={item.Id}
|
||||
loading={false}
|
||||
lessonPreview={{
|
||||
Title: item.Title,
|
||||
ThumbnailUrl: item.ThumbnailUrl,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{publishedItems.length > 0 && (
|
||||
<>
|
||||
{publishedItems.map((item, index) => (
|
||||
<LessonPreviewCard
|
||||
key={index}
|
||||
mode="view"
|
||||
lessonId={item.Id}
|
||||
loading={false}
|
||||
lessonSettings={{
|
||||
Title: item.Title,
|
||||
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 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 (
|
||||
<>
|
||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={
|
||||
<HeaderBar />
|
||||
} />
|
||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -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 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 <MyErrorResult />;
|
||||
|
||||
return (
|
||||
<MyTable loading={isLoading} columns={getTableContent()} dataSource={getTableItems()} pagination={false} />
|
||||
);
|
||||
};
|
||||
|
||||
export default function Team() {
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Team" subtitle="MANAGE" headerBar={
|
||||
<HeaderBar />
|
||||
} />
|
||||
<MyBanner title="Team" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
|
||||
<MyContainer>
|
||||
<h1>Team</h1>
|
||||
<MyContainer
|
||||
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>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
import { Content } from "antd/es/layout/layout";
|
||||
|
||||
export function MyContainer({ children }: { children: React.ReactNode }) {
|
||||
return <Content style={{ padding: 12 }}>{children}</Content>;
|
||||
export function MyContainer({
|
||||
children,
|
||||
style = {},
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
style?: React.CSSProperties;
|
||||
}) {
|
||||
return <Content style={{ padding: 12, ...style }}>{children}</Content>;
|
||||
}
|
||||
|
||||
export function MyCenteredContainer({
|
||||
|
|
|
@ -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({
|
|||
<UploadWrapper>
|
||||
<img
|
||||
src={
|
||||
lessonPreview.ThumbnailUrl === ""
|
||||
lessonSettings.ThumbnailUrl === ""
|
||||
? `${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`
|
||||
: getImageUrl(lessonPreview.ThumbnailUrl)
|
||||
: getImageUrl(lessonSettings.ThumbnailUrl)
|
||||
}
|
||||
alt="lesson thumbnail"
|
||||
className={styles.img}
|
||||
|
@ -73,7 +73,7 @@ export default function MyLessonPreviewCard({
|
|||
<Flex vertical justify="center" style={{ width: "100%" }}>
|
||||
{mode === "view" ? (
|
||||
<div>
|
||||
<div className={styles.title}>{lessonPreview.Title}</div>
|
||||
<div className={styles.title}>{lessonSettings.Title}</div>
|
||||
<CommentOutlined /> 12 comments
|
||||
</div>
|
||||
) : (
|
||||
|
@ -88,7 +88,7 @@ export default function MyLessonPreviewCard({
|
|||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{lessonPreview.Title}
|
||||
{lessonSettings.Title}
|
||||
</Typography.Title>
|
||||
)}
|
||||
</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}`}
|
||||
onChange={onChange}
|
||||
beforeUpload={beforeUpload}
|
||||
|
||||
>
|
||||
{children}
|
||||
</Upload>
|
||||
|
|
Loading…
Reference in New Issue