lms-frontend/src/core/components/SideMenu/index.tsx

430 lines
11 KiB
TypeScript

import {
ControlOutlined,
MessageOutlined,
QuestionCircleOutlined,
SettingOutlined,
SnippetsOutlined,
TeamOutlined,
WalletOutlined,
} from "@ant-design/icons";
import { Divider, Flex, Form, Menu, Select, Typography } from "antd";
import { useEffect, useState } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import {
setIsSideMenuCollapsed,
setSideMenuComponentFirstRender,
sideMenuComponentFirstRender,
} from "./sideMenuSlice";
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,
currentLessonId,
lessonState,
} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
import { Component, componentsGroups } from "features/Lessons/components";
import { darkMode, logoUrl } from "core/reducers/appSlice";
import { DndContext, DragOverlay, useDraggable } from "@dnd-kit/core";
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();
const [selectedKeys, setSelectedKeys] = useState("/");
const [openKeys, setOpenKeys] = useState([""]);
const dispatch = useDispatch();
const componentFirstRender = useSelector(sideMenuComponentFirstRender);
const appLogoUrl = useSelector(logoUrl);
const navigate = useNavigate();
const getFirstMenuItems = (): ItemType<MenuItemType>[] => {
let items: ItemType<MenuItemType>[] = [];
// overview
let overviewGroup: ItemType<MenuItemType> = {
key: "overviewGroup",
label: "OVERVIEW",
type: "group",
children: [],
};
if (overviewGroup.children) {
overviewGroup.children.push({
key: Constants.ROUTE_PATHS.LESSIONS.ROOT,
label: "Lessons",
icon: <SnippetsOutlined />,
});
}
items.push(overviewGroup);
// organization
let organizationGroup: ItemType<MenuItemType> = {
key: "organizationGroup",
label: "ORGANIZATION",
type: "group",
children: [],
};
if (organizationGroup.children) {
organizationGroup.children.push({
key: Constants.ROUTE_PATHS.ORGANIZATION_TEAM,
label: "Team",
icon: <TeamOutlined />,
});
organizationGroup.children.push({
key: Constants.ROUTE_PATHS.ORGANIZATION_ROLES,
label: "Roles",
icon: <ControlOutlined />,
});
organizationGroup.children.push({
key: Constants.ROUTE_PATHS.ORGANIZATION_SETTINGS,
label: "Settings",
icon: <SettingOutlined />,
});
}
items.push(organizationGroup);
return items;
};
const getSecondMenuItems = (): ItemType<MenuItemType>[] => {
let items: ItemType<MenuItemType>[] = [];
// support
items.push({
key: Constants.ROUTE_PATHS.WHATS_NEW,
label: "What's New",
icon: <QuestionCircleOutlined />,
});
// feedback
items.push({
key: Constants.ROUTE_PATHS.SUGGEST_FEATURE,
label: "Suggest a Feature",
icon: <MessageOutlined />,
});
// payment plan
items.push({
key: Constants.ROUTE_PATHS.CONTACT_SUPPORT,
label: "Contact Support",
icon: <WalletOutlined />,
});
return items;
};
useEffect(() => {
const pathname = location.pathname;
setSelectedKeys(pathname);
let path = pathname.split("/");
if (path.length > 2) {
// /store/:storeId/:subPage - open the store menu
setOpenKeys([`/${path[1]}/${path[2]}`]);
}
// auto close sideMenu on mobile
// this will prevent to auto close sideMenu on first render as the useEffects will be called after the first render
if (componentFirstRender) {
dispatch(setSideMenuComponentFirstRender(false));
} else if (document.body.clientWidth < BreakpointLgWidth) {
dispatch(setIsSideMenuCollapsed(true));
}
}, [location.pathname]);
return (
<div
style={{
display: "flex",
justifyContent: "space-between",
flexDirection: "column",
height: "100%",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
overflowY: "hidden",
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: 10,
}}
></div>
<div>
<Flex justify="center" style={{ paddingBottom: 24, width: "100%" }}>
<img
src={`${Constants.STATIC_CONTENT_ADDRESS}${appLogoUrl}`}
alt="logo"
style={{ height: 80 }}
/>
</Flex>
<Menu
mode="inline"
onClick={(item) => navigate(item.key)}
theme="light"
selectedKeys={[selectedKeys]}
items={getFirstMenuItems()}
openKeys={openKeys}
onOpenChange={(openKeys) =>
setOpenKeys(
openKeys[openKeys.length - 1]
? [openKeys[openKeys.length - 1]]
: []
)
}
/>
</div>
</div>
<div>
<Divider style={{ margin: 0 }} />
<Menu
selectable={true}
selectedKeys={[selectedKeys]}
mode="vertical"
onClick={(item) => navigate(item.key)}
items={getSecondMenuItems()}
/>
</div>
</div>
);
}
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 (
<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);
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;
}
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>
);
}
export function DraggableCreateComponent({
component,
}: {
component: Component;
}) {
const dispatch = useDispatch();
/*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,
}
}
>
<CreateComponent component={component} />
</div>
</>
);
}
function CreateComponent({ component }: { component: Component }) {
const dispatch = useDispatch();
const isDarkMode = useSelector(darkMode);
const currentLnId = useSelector(currentLessonId);
const [reqAddLessonContent] = useAddLessonContentMutation();
return (
<Flex
vertical
align="center"
justify="center"
style={{
backgroundColor: !isDarkMode ? "#f0f2f5" : "#1f1f1f",
height: 80,
width: 80,
cursor: "pointer",
}}
onClick={async () => {
console.log("insert component", 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 ? (
<div>
<img
src={component.thumbnail}
style={{
width: 40,
filter:
isDarkMode && component.invertThumbnailAtDarkmode
? "invert(1)"
: "invert(0)",
}}
/>
</div>
) : null}
<Typography.Text style={{ fontSize: 12 }}>
{component.name}
</Typography.Text>
</Flex>
);
}
//console.log("insert component", component.type);
//dispatch(addLessonContent(component.type));