comments and youtube and videos on editor
parent
00dee1ba9e
commit
e6b9a6d0e8
|
@ -20,6 +20,7 @@
|
||||||
"@types/node": "^16.18.106",
|
"@types/node": "^16.18.106",
|
||||||
"@types/react": "^18.3.4",
|
"@types/react": "^18.3.4",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vidstack/react": "^1.12.9",
|
||||||
"antd": "^5.20.3",
|
"antd": "^5.20.3",
|
||||||
"antd-img-crop": "^4.23.0",
|
"antd-img-crop": "^4.23.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
@ -2744,6 +2745,31 @@
|
||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.7.tgz",
|
||||||
|
"integrity": "sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.6.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.10.tgz",
|
||||||
|
"integrity": "sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.6.0",
|
||||||
|
"@floating-ui/utils": "^0.2.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.7.tgz",
|
||||||
|
"integrity": "sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.14",
|
"version": "0.11.14",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||||
|
@ -5226,6 +5252,23 @@
|
||||||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/@vidstack/react": {
|
||||||
|
"version": "1.12.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@vidstack/react/-/react-1.12.9.tgz",
|
||||||
|
"integrity": "sha512-2YBkMN590u20P9JVw6EoaAegVz4YP7utxeRXuDkzvn60UG8Ky6v4CdywFaBAHBrxyRefiCJTLB5noDmIRyVplg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.6.10",
|
||||||
|
"media-captions": "^1.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.0.0",
|
||||||
|
"react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@webassemblyjs/ast": {
|
"node_modules/@webassemblyjs/ast": {
|
||||||
"version": "1.12.1",
|
"version": "1.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
|
||||||
|
@ -13878,6 +13921,15 @@
|
||||||
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
|
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/media-captions": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-captions/-/media-captions-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-cyDNmuZvvO4H27rcBq2Eudxo9IZRDCOX/I7VEyqbxsEiD2Ei7UYUhG/Sc5fvMZjmathgz3fEK7iAKqvpY+Ux1w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/media-typer": {
|
"node_modules/media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"@types/node": "^16.18.106",
|
"@types/node": "^16.18.106",
|
||||||
"@types/react": "^18.3.4",
|
"@types/react": "^18.3.4",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vidstack/react": "^1.12.9",
|
||||||
"antd": "^5.20.3",
|
"antd": "^5.20.3",
|
||||||
"antd-img-crop": "^4.23.0",
|
"antd-img-crop": "^4.23.0",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
|
|
|
@ -1,36 +1,27 @@
|
||||||
import { Avatar, Flex } from "antd";
|
import { Avatar, Dropdown, Flex } from 'antd';
|
||||||
import {
|
import { isSideMenuCollapsed, setIsSideMenuCollapsed } from '../SideMenu/sideMenuSlice';
|
||||||
isSideMenuCollapsed,
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
setIsSideMenuCollapsed,
|
import { EditOutlined, EyeOutlined, LeftOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, SunOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
} from "../SideMenu/sideMenuSlice";
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { darkMode, setDarkMode } from '../../reducers/appSlice';
|
||||||
import {
|
import styles from './styles.module.css';
|
||||||
EditOutlined,
|
import { Constants } from 'core/utils/utils';
|
||||||
EyeOutlined,
|
|
||||||
LeftOutlined,
|
|
||||||
MenuFoldOutlined,
|
|
||||||
MenuUnfoldOutlined,
|
|
||||||
MoonOutlined,
|
|
||||||
SunOutlined,
|
|
||||||
UserOutlined,
|
|
||||||
} from "@ant-design/icons";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { darkMode, setDarkMode } from "../../reducers/appSlice";
|
|
||||||
import styles from "./styles.module.css";
|
|
||||||
|
|
||||||
type HeaderBarProps = {
|
type HeaderBarProps = {
|
||||||
theme?: "light" | "dark";
|
theme?: 'light' | 'dark';
|
||||||
onView?: () => void;
|
onView?: () => void;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
backTo?: string;
|
backTo?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
|
export default function HeaderBar(props: HeaderBarProps = { theme: 'light' }) {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||||
const isDarkMode = useSelector(darkMode);
|
const isDarkMode = useSelector(darkMode);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
justify="space-between"
|
justify="space-between"
|
||||||
|
@ -42,26 +33,13 @@ export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex align="center" gap={16}>
|
<Flex align="center" gap={16}>
|
||||||
<div
|
<div className={props.theme === 'light' ? styles.containerLight : styles.containerDark} style={{ borderRadius: 28, padding: 4 }}>
|
||||||
className={
|
|
||||||
props.theme === "light"
|
|
||||||
? styles.containerLight
|
|
||||||
: styles.containerDark
|
|
||||||
}
|
|
||||||
style={{ borderRadius: 28, padding: 4 }}
|
|
||||||
>
|
|
||||||
{isCollpased ? (
|
{isCollpased ? (
|
||||||
<div
|
<div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(false))}>
|
||||||
className={styles.iconContainer}
|
|
||||||
onClick={() => dispatch(setIsSideMenuCollapsed(false))}
|
|
||||||
>
|
|
||||||
<MenuUnfoldOutlined className={styles.icon} />
|
<MenuUnfoldOutlined className={styles.icon} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(true))}>
|
||||||
className={styles.iconContainer}
|
|
||||||
onClick={() => dispatch(setIsSideMenuCollapsed(true))}
|
|
||||||
>
|
|
||||||
<MenuFoldOutlined className={styles.icon} />
|
<MenuFoldOutlined className={styles.icon} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -79,9 +57,7 @@ export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
|
||||||
|
|
||||||
<Flex
|
<Flex
|
||||||
align="center"
|
align="center"
|
||||||
className={
|
className={props.theme === 'light' ? styles.containerLight : styles.containerDark}
|
||||||
props.theme === "light" ? styles.containerLight : styles.containerDark
|
|
||||||
}
|
|
||||||
style={{
|
style={{
|
||||||
borderRadius: 28,
|
borderRadius: 28,
|
||||||
paddingLeft: 6,
|
paddingLeft: 6,
|
||||||
|
@ -104,21 +80,34 @@ export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isDarkMode ? (
|
{isDarkMode ? (
|
||||||
<div
|
<div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(false))}>
|
||||||
className={styles.iconContainer}
|
|
||||||
onClick={() => dispatch(setDarkMode(false))}
|
|
||||||
>
|
|
||||||
<SunOutlined className={styles.icon} />
|
<SunOutlined className={styles.icon} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(true))}>
|
||||||
className={styles.iconContainer}
|
|
||||||
onClick={() => dispatch(setDarkMode(true))}
|
|
||||||
>
|
|
||||||
<MoonOutlined className={styles.icon} />
|
<MoonOutlined className={styles.icon} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: '1',
|
||||||
|
label: 'Profile',
|
||||||
|
icon: <UserOutlined />,
|
||||||
|
onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: '2',
|
||||||
|
label: 'Logout',
|
||||||
|
icon: <LogoutOutlined />,
|
||||||
|
danger: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Avatar size="default" icon={<UserOutlined />} />
|
<Avatar size="default" icon={<UserOutlined />} />
|
||||||
|
</Dropdown>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { createApi } from '@reduxjs/toolkit/query/react';
|
||||||
|
import { baseQueryWithErrorHandling } from 'core/helper/api';
|
||||||
|
import { TeamMember, OrganizationSettings } from 'core/types/organization';
|
||||||
|
|
||||||
|
export const organizationApi = createApi({
|
||||||
|
reducerPath: 'organizationApi',
|
||||||
|
baseQuery: baseQueryWithErrorHandling,
|
||||||
|
endpoints: (builder) => ({
|
||||||
|
getTeam: builder.query<TeamMember[], undefined>({
|
||||||
|
query: () => ({
|
||||||
|
url: 'organization/team/members',
|
||||||
|
method: 'GET',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getOrganizationSettings: builder.query<OrganizationSettings, undefined>({
|
||||||
|
query: () => ({
|
||||||
|
url: 'organization/settings',
|
||||||
|
method: 'GET',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { useGetTeamQuery, useGetOrganizationSettingsQuery } = organizationApi;
|
|
@ -1,18 +0,0 @@
|
||||||
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;
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { configureStore } from "@reduxjs/toolkit";
|
import { configureStore } from '@reduxjs/toolkit';
|
||||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
import { setupListeners } from '@reduxjs/toolkit/query';
|
||||||
import { sideMenuSlice } from "../components/SideMenu/sideMenuSlice";
|
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";
|
import { organizationApi } from 'core/services/organization';
|
||||||
|
|
||||||
const makeStore = (/* preloadedState */) => {
|
const makeStore = (/* preloadedState */) => {
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
|
@ -13,14 +13,10 @@ 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,
|
[organizationApi.reducerPath]: organizationApi.reducer,
|
||||||
},
|
},
|
||||||
// preloadedState,
|
// preloadedState,
|
||||||
middleware: (getDefaultMiddleware) =>
|
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(lessonsApi.middleware, organizationApi.middleware),
|
||||||
getDefaultMiddleware().concat(
|
|
||||||
lessonsApi.middleware,
|
|
||||||
teamApi.middleware
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setupListeners(store.dispatch);
|
setupListeners(store.dispatch);
|
||||||
|
|
|
@ -32,3 +32,24 @@ export interface UpdateLessonPreviewThumbnail {
|
||||||
lessonId: string;
|
lessonId: string;
|
||||||
formData: FormData;
|
formData: FormData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LessonQuestion {
|
||||||
|
Id: string;
|
||||||
|
LessionId: string;
|
||||||
|
Question: string;
|
||||||
|
Likes: number;
|
||||||
|
CreatorUserId: string;
|
||||||
|
CreatedAt: string;
|
||||||
|
UpdatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LessonQuestionReply {
|
||||||
|
Id: string;
|
||||||
|
QuestionId: string;
|
||||||
|
ReplyId?: string;
|
||||||
|
Reply: string;
|
||||||
|
Likes: number;
|
||||||
|
CreatorUserId: string;
|
||||||
|
CreatedAt: string;
|
||||||
|
UpdatedAt: string;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
export interface TeamMember {
|
||||||
|
Id: string;
|
||||||
|
FirstName: string;
|
||||||
|
LastName: string;
|
||||||
|
Email: string;
|
||||||
|
Role: string;
|
||||||
|
Status: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrganizationSettings {
|
||||||
|
Subdomain: string;
|
||||||
|
CompanyName: string;
|
||||||
|
PrimaryColor: string;
|
||||||
|
LogoUrl: string;
|
||||||
|
BannerUrl: string;
|
||||||
|
}
|
|
@ -1,8 +0,0 @@
|
||||||
export interface TeamMember {
|
|
||||||
Id: string;
|
|
||||||
FirstName: string;
|
|
||||||
LastName: string;
|
|
||||||
Email: string;
|
|
||||||
Role: string;
|
|
||||||
Status: string;
|
|
||||||
}
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Buffer } from "buffer";
|
import { Buffer } from 'buffer';
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
const wssProtocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
const wssProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||||
|
|
||||||
export const Constants = {
|
export const Constants = {
|
||||||
API_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/v1`,
|
API_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/v1`,
|
||||||
|
@ -9,29 +9,25 @@ export const Constants = {
|
||||||
STATIC_CONTENT_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/static`,
|
STATIC_CONTENT_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/static`,
|
||||||
ROUTE_PATHS: {
|
ROUTE_PATHS: {
|
||||||
LESSIONS: {
|
LESSIONS: {
|
||||||
ROOT: "/lessons",
|
ROOT: '/lessons',
|
||||||
PAGE: "/lessons/:lessonId",
|
PAGE: '/lessons/:lessonId',
|
||||||
PAGE_EDITOR: "/lessons/:lessonId/editor",
|
PAGE_EDITOR: '/lessons/:lessonId/editor',
|
||||||
},
|
},
|
||||||
ORGANIZATION_TEAM: "/team",
|
ORGANIZATION_TEAM: '/team',
|
||||||
ORGANIZATION_TEAM_CREATE_USER: "/team/create-user",
|
ORGANIZATION_TEAM_CREATE_USER: '/team/create-user',
|
||||||
ORGANIZATION_ROLES: "/roles",
|
ORGANIZATION_ROLES: '/roles',
|
||||||
ORGANIZATION_SETTINGS: "/organization",
|
ORGANIZATION_SETTINGS: '/organization',
|
||||||
ACCOUNT_SETTINGS: "/account",
|
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',
|
||||||
},
|
},
|
||||||
STYLES: {
|
STYLES: {
|
||||||
BLACK: "#000",
|
BLACK: '#000',
|
||||||
},
|
},
|
||||||
MAX_IMAGE_SIZE: 25 * 1024 * 1024, // 25MB
|
MAX_IMAGE_SIZE: 25 * 1024 * 1024, // 25MB
|
||||||
ACCEPTED_IMAGE_FILE_TYPES: [
|
ACCEPTED_IMAGE_FILE_TYPES: ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'],
|
||||||
"image/png",
|
ACCEPTED_VIDEO_FILE_TYPES: ['video/mp4', 'video/webm', 'video/mkv'],
|
||||||
"image/jpeg",
|
|
||||||
"image/jpg",
|
|
||||||
"image/webp",
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// used for sideMenu
|
// used for sideMenu
|
||||||
|
@ -53,20 +49,20 @@ export function getImageUrl(imageName: string) {
|
||||||
export const BrowserTabSession = GetUuid();
|
export const BrowserTabSession = GetUuid();
|
||||||
|
|
||||||
export function getUserSessionFromLocalStorage() {
|
export function getUserSessionFromLocalStorage() {
|
||||||
return localStorage.getItem("session");
|
return localStorage.getItem('session');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EncodeStringToBase64(value: string) {
|
export function EncodeStringToBase64(value: string) {
|
||||||
return Buffer.from(value).toString("base64");
|
return Buffer.from(value).toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DecodedBase64ToString(value: string) {
|
export function DecodedBase64ToString(value: string) {
|
||||||
return Buffer.from(value, "base64").toString();
|
return Buffer.from(value, 'base64').toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleLogout() {
|
export function handleLogout() {
|
||||||
localStorage.removeItem("session");
|
localStorage.removeItem('session');
|
||||||
window.location.href = "/";
|
window.location.href = '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const myFetchContentType = {
|
export const myFetchContentType = {
|
||||||
|
@ -74,14 +70,14 @@ export const myFetchContentType = {
|
||||||
FORM_DATA: 1,
|
FORM_DATA: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||||
|
|
||||||
interface MyFetchOptions<TRequest = any, TResponse = any> {
|
interface MyFetchOptions<TRequest = any, TResponse = any> {
|
||||||
url?: string;
|
url?: string;
|
||||||
method?: Method;
|
method?: Method;
|
||||||
body?: TRequest | null;
|
body?: TRequest | null;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
contentType?: "JSON" | "FORM_DATA";
|
contentType?: 'JSON' | 'FORM_DATA';
|
||||||
fetchUrl?: string;
|
fetchUrl?: string;
|
||||||
ignoreUnauthorized?: boolean;
|
ignoreUnauthorized?: boolean;
|
||||||
notificationApi?: any; // Passen Sie dies je nach Bedarf an
|
notificationApi?: any; // Passen Sie dies je nach Bedarf an
|
||||||
|
@ -89,26 +85,26 @@ interface MyFetchOptions<TRequest = any, TResponse = any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function myFetch<TRequest = any, TResponse = any>({
|
export function myFetch<TRequest = any, TResponse = any>({
|
||||||
url = "",
|
url = '',
|
||||||
method = "GET",
|
method = 'GET',
|
||||||
body = null,
|
body = null,
|
||||||
headers = {},
|
headers = {},
|
||||||
contentType = "JSON",
|
contentType = 'JSON',
|
||||||
fetchUrl = Constants.API_ADDRESS,
|
fetchUrl = Constants.API_ADDRESS,
|
||||||
ignoreUnauthorized = false,
|
ignoreUnauthorized = false,
|
||||||
notificationApi = null,
|
notificationApi = null,
|
||||||
t = null,
|
t = null,
|
||||||
}: MyFetchOptions<TRequest, TResponse>): Promise<TResponse> {
|
}: MyFetchOptions<TRequest, TResponse>): Promise<TResponse> {
|
||||||
const getContentType = () => {
|
const getContentType = () => {
|
||||||
if (contentType === "JSON") return "application/json";
|
if (contentType === 'JSON') return 'application/json';
|
||||||
|
|
||||||
return "multipart/form-data";
|
return 'multipart/form-data';
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBody = () => {
|
const getBody = () => {
|
||||||
if (!body) return null;
|
if (!body) return null;
|
||||||
|
|
||||||
if (contentType === "JSON") return JSON.stringify(body);
|
if (contentType === 'JSON') return JSON.stringify(body);
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
|
@ -122,15 +118,15 @@ export function myFetch<TRequest = any, TResponse = any>({
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
"X-Authorization": getUserSessionFromLocalStorage() || "",
|
'X-Authorization': getUserSessionFromLocalStorage() || '',
|
||||||
"Content-Type": getContentType(),
|
'Content-Type': getContentType(),
|
||||||
...headers,
|
...headers,
|
||||||
},
|
},
|
||||||
body: getBody(),
|
body: getBody(),
|
||||||
signal: signal,
|
signal: signal,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fetchUrl === "") {
|
if (fetchUrl === '') {
|
||||||
fetchUrl = Constants.API_ADDRESS;
|
fetchUrl = Constants.API_ADDRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +135,7 @@ export function myFetch<TRequest = any, TResponse = any>({
|
||||||
// if status is not in range 200-299
|
// if status is not in range 200-299
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (!ignoreUnauthorized && response.status === 401) {
|
if (!ignoreUnauthorized && response.status === 401) {
|
||||||
console.error("Unauthorized");
|
console.error('Unauthorized');
|
||||||
// TODO: check here
|
// TODO: check here
|
||||||
//setUserSessionToLocalStorage("");
|
//setUserSessionToLocalStorage("");
|
||||||
//window.location.href = "/";
|
//window.location.href = "/";
|
||||||
|
@ -149,14 +145,14 @@ export function myFetch<TRequest = any, TResponse = any>({
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if response is json
|
// check if response is json
|
||||||
if (response.headers.get("content-type")?.includes("application/json")) {
|
if (response.headers.get('content-type')?.includes('application/json')) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.text();
|
return response.text();
|
||||||
})
|
})
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
console.error("Error", error);
|
console.error('Error', error);
|
||||||
|
|
||||||
// ignore errors here as they are handled in the components
|
// ignore errors here as they are handled in the components
|
||||||
if (error === 400) throw error;
|
if (error === 400) throw error;
|
||||||
|
|
|
@ -2,6 +2,7 @@ import {
|
||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
|
Descriptions,
|
||||||
Divider,
|
Divider,
|
||||||
Flex,
|
Flex,
|
||||||
Form,
|
Form,
|
||||||
|
@ -18,78 +19,31 @@ import ColorPicker from "antd/es/color-picker";
|
||||||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||||
import Meta from "antd/es/card/Meta";
|
import Meta from "antd/es/card/Meta";
|
||||||
|
|
||||||
export function AccountSettingsAdmin() {
|
export default function AccountSettings({ isAdmin }: { isAdmin?: boolean }) {
|
||||||
return (
|
function AdminWrapper({ children }: { children: React.ReactNode }) {
|
||||||
<>
|
if (!isAdmin) {
|
||||||
<MyBanner
|
return <>{children}</>;
|
||||||
title="Account Settings"
|
}
|
||||||
subtitle="MANAGE"
|
|
||||||
headerBar={<HeaderBar />}
|
return (
|
||||||
/>
|
<Form layout="vertical" style={{ marginTop: 24 }}>
|
||||||
|
{children}
|
||||||
<MyMiddleCard title="My Profile">
|
</Form>
|
||||||
<Flex vertical gap={16} >
|
);
|
||||||
<Card
|
}
|
||||||
styles={{
|
|
||||||
body: {
|
function TextItem({ value, name }: { value: string; name: string }) {
|
||||||
padding: 16,
|
if (!isAdmin) {
|
||||||
},
|
return <>{value}</>;
|
||||||
}}
|
}
|
||||||
>
|
|
||||||
<Meta
|
return (
|
||||||
avatar={
|
<Form.Item name={name} style={{ width: "100%" }} required>
|
||||||
<Avatar
|
<Input defaultValue={value} />
|
||||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
</Form.Item>
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<MyBanner
|
<MyBanner
|
||||||
|
@ -118,7 +72,7 @@ export default function AccountSettings() {
|
||||||
description="Lead"
|
description="Lead"
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
<Card
|
{/*<Card
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
|
@ -144,6 +98,37 @@ export default function AccountSettings() {
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
</Card>*/}
|
||||||
|
<Card
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: 16,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AdminWrapper>
|
||||||
|
<Descriptions
|
||||||
|
title="Personal Information"
|
||||||
|
layout="vertical"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
key: "1",
|
||||||
|
label: "First name",
|
||||||
|
children: <TextItem value="Jorg" name="firstName" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "2",
|
||||||
|
label: "Last name",
|
||||||
|
children: <TextItem value="Kreth" name="lastName" />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "3",
|
||||||
|
label: "Email",
|
||||||
|
children: <TextItem value="julian@xx.com" name="email" />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</AdminWrapper>
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</Flex>
|
||||||
</MyMiddleCard>
|
</MyMiddleCard>
|
||||||
|
|
|
@ -20,7 +20,7 @@ interface SignInFetchResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SignIn() {
|
export default function SignIn() {
|
||||||
const [form] = useForm();
|
const [form] = useForm<FieldType>();
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
|
|
@ -1,25 +1,23 @@
|
||||||
import { Button, Flex } from "antd";
|
import { Button, Flex } from 'antd';
|
||||||
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';
|
||||||
import { Constants } from "../../../core/utils/utils";
|
import { Constants } from '../../../core/utils/utils';
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
import MySpin from "shared/components/MySpin";
|
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";
|
import MyMiddleCard from 'shared/components/MyMiddleCard';
|
||||||
import { Converter } from "../converter";
|
import { Converter } from '../converter';
|
||||||
|
import Questions from '../Questions';
|
||||||
|
|
||||||
const LessonContents: React.FC = () => {
|
const LessonContents: React.FC = () => {
|
||||||
const { lessonId } = useParams();
|
const { lessonId } = useParams();
|
||||||
|
|
||||||
const { data, error, isLoading } = useGetLessonContentsQuery(
|
const { data, error, isLoading } = useGetLessonContentsQuery(lessonId as string, {
|
||||||
lessonId as string,
|
|
||||||
{
|
|
||||||
refetchOnMountOrArgChange: true,
|
refetchOnMountOrArgChange: true,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (isLoading) return <MySpin />;
|
if (isLoading) return <MySpin />;
|
||||||
if (error) return <MyErrorResult />;
|
if (error) return <MyErrorResult />;
|
||||||
|
@ -43,13 +41,15 @@ export default function LessonPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeaderBar
|
<HeaderBar theme="light" backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT} onEdit={() => navigate(`${location.pathname}/editor`)} />
|
||||||
theme="light"
|
|
||||||
backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT}
|
|
||||||
onEdit={() => navigate(`${location.pathname}/editor`)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<MyMiddleCard>
|
<MyMiddleCard
|
||||||
|
outOfCardChildren={
|
||||||
|
<div style={{ marginTop: 24 }}>
|
||||||
|
<Questions lessionID={'lessionID'} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
<LessonContents />
|
<LessonContents />
|
||||||
|
|
||||||
<Flex justify="right">
|
<Flex justify="right">
|
||||||
|
|
|
@ -1,32 +1,31 @@
|
||||||
import { DndContext, DragEndEvent, useDroppable } from "@dnd-kit/core";
|
import { closestCenter, closestCorners, DndContext, DragEndEvent, DragOverlay, MeasuringStrategy, rectIntersection, useDroppable } from '@dnd-kit/core';
|
||||||
import {
|
import { verticalListSortingStrategy, SortableContext, rectSwappingStrategy, rectSortingStrategy } from '@dnd-kit/sortable';
|
||||||
verticalListSortingStrategy,
|
import SortableEditorItem from './SortableEditorItem';
|
||||||
SortableContext,
|
import { store } from 'core/store/store';
|
||||||
} from "@dnd-kit/sortable";
|
|
||||||
import SortableEditorItem from "./SortableEditorItem";
|
|
||||||
import { store } from "core/store/store";
|
|
||||||
|
|
||||||
import {
|
import { restrictToVerticalAxis, restrictToWindowEdges, snapCenterToCursor } from '@dnd-kit/modifiers';
|
||||||
restrictToVerticalAxis,
|
import { currentLessonId, onDragHandler } from './lessonPageEditorSlice';
|
||||||
restrictToWindowEdges,
|
import { LessonContent } from 'core/types/lesson';
|
||||||
} from "@dnd-kit/modifiers";
|
import { useUpdateLessonContentPositionMutation } from 'core/services/lessons';
|
||||||
import { currentLessonId, onDragHandler } from "./lessonPageEditorSlice";
|
import { useSelector } from 'react-redux';
|
||||||
import { LessonContent } from "core/types/lesson";
|
import React from 'react';
|
||||||
import { useUpdateLessonContentPositionMutation } from "core/services/lessons";
|
import { Typography } from 'antd';
|
||||||
import { useSelector } from "react-redux";
|
import { HolderOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
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 currentLnId = useSelector(currentLessonId);
|
||||||
|
|
||||||
const [reqUpdateLessonContentPosition] =
|
const [reqUpdateLessonContentPosition] = useUpdateLessonContentPositionMutation();
|
||||||
useUpdateLessonContentPositionMutation();
|
|
||||||
|
const [isDragging, setIsDragging] = React.useState(false);
|
||||||
|
|
||||||
const itemIDs = items.map((item) => item.Id);
|
const itemIDs = items.map((item) => item.Id);
|
||||||
|
|
||||||
const handleDragEnd = (event: DragEndEvent) => {
|
const handleDragEnd = (event: DragEndEvent) => {
|
||||||
console.log("drag end", event);
|
console.log('drag end', event);
|
||||||
|
setIsDragging(false);
|
||||||
|
|
||||||
if (!event.over) return;
|
if (!event.over) return;
|
||||||
|
|
||||||
|
@ -55,20 +54,21 @@ const Droppable = ({ items }: { items: LessonContent[] }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}
|
modifiers={[snapCenterToCursor]}
|
||||||
|
collisionDetection={closestCorners}
|
||||||
|
onDragStart={() => {
|
||||||
|
setIsDragging(true);
|
||||||
|
}}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
>
|
>
|
||||||
<SortableContext
|
<SortableContext id={droppableID} items={itemIDs} strategy={verticalListSortingStrategy}>
|
||||||
id={droppableID}
|
|
||||||
items={itemIDs}
|
|
||||||
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>
|
||||||
|
<DragOverlay>{isDragging ? <HolderOutlined style={{ cursor: 'grabbing' }} /> : null}</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,31 +1,20 @@
|
||||||
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 {
|
import { HolderOutlined, DeleteOutlined, CameraOutlined, FolderOpenOutlined } from '@ant-design/icons';
|
||||||
HolderOutlined,
|
import { Flex } from 'antd';
|
||||||
DeleteOutlined,
|
import { currentLessonId, deleteLessonContent, updateLessonContent } from './lessonPageEditorSlice';
|
||||||
CameraOutlined,
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
FolderOpenOutlined,
|
import { getComponentByType } from '../components';
|
||||||
} from "@ant-design/icons";
|
import { LessonContent } from 'core/types/lesson';
|
||||||
import { Flex } from "antd";
|
import './styles.module.css';
|
||||||
import {
|
import { Converter } from '../converter';
|
||||||
currentLessonId,
|
import { useDeleteLessonContentMutation } from 'core/services/lessons';
|
||||||
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 { attributes, listeners, setNodeRef, transform, transition } =
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: lnContent.Id });
|
||||||
useSortable({ id: lnContent.Id, animateLayoutChanges });
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
@ -48,20 +37,24 @@ const SortableEditorItem = (props: { item: LessonContent }) => {
|
||||||
{...attributes}
|
{...attributes}
|
||||||
>
|
>
|
||||||
<Flex key={lnContent.Id}>
|
<Flex key={lnContent.Id}>
|
||||||
|
<Flex>
|
||||||
<HolderOutlined
|
<HolderOutlined
|
||||||
style={{
|
style={{
|
||||||
paddingLeft: 8,
|
paddingLeft: 8,
|
||||||
paddingRight: 8,
|
paddingRight: 8,
|
||||||
touchAction: "none",
|
touchAction: 'none',
|
||||||
cursor: "move",
|
cursor: 'grab',
|
||||||
|
opacity: isDragging ? 0 : 1,
|
||||||
}}
|
}}
|
||||||
{...listeners}
|
{...listeners}
|
||||||
/>
|
/>
|
||||||
|
</Flex>
|
||||||
|
<Flex style={{ overflow: 'hidden', width: '100%', transition: '', boxShadow: isDragging ? 'rgba(0, 0, 0, 0.35) 0px 5px 15px;' : '' }}>
|
||||||
<Converter
|
<Converter
|
||||||
mode="edititable"
|
mode="edititable"
|
||||||
lessonContent={lnContent}
|
lessonContent={lnContent}
|
||||||
onEdit={(data) => {
|
onEdit={(data) => {
|
||||||
console.log("edit", lnContent.Id, data);
|
console.log('edit', lnContent.Id, data);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
updateLessonContent({
|
updateLessonContent({
|
||||||
|
@ -71,23 +64,14 @@ const SortableEditorItem = (props: { item: LessonContent }) => {
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<Flex vertical justify="center">
|
<Flex vertical justify="center" style={{ paddingLeft: 12 }}>
|
||||||
{component.uploadImage ? (
|
|
||||||
<div className="EditorActionIcon">
|
|
||||||
<CameraOutlined />
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
{component.uploadFileTypes ? (
|
|
||||||
<div className="EditorActionIcon">
|
|
||||||
<FolderOpenOutlined className="EditorActionIcon" />{" "}
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
<div className="EditorActionIcon">
|
<div className="EditorActionIcon">
|
||||||
<DeleteOutlined
|
<DeleteOutlined
|
||||||
className="EditorActionIcon"
|
className="EditorActionIcon"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("delete", lnContent.Id);
|
console.log('delete', lnContent.Id);
|
||||||
dispatch(deleteLessonContent(lnContent.Id));
|
dispatch(deleteLessonContent(lnContent.Id));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -107,4 +91,17 @@ const SortableEditorItem = (props: { item: LessonContent }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
{component.uploadImage ? (
|
||||||
|
<div className="EditorActionIcon">
|
||||||
|
<CameraOutlined />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{component.uploadFileTypes ? (
|
||||||
|
<div className="EditorActionIcon">
|
||||||
|
<FolderOpenOutlined className="EditorActionIcon" />{' '}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
*/
|
||||||
|
|
||||||
export default SortableEditorItem;
|
export default SortableEditorItem;
|
||||||
|
|
|
@ -1,38 +1,24 @@
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import {
|
import { lessonContents, lessonThumbnail, setCurrentLessonId, setEditorActive, setLessonContents, setLessonState } from './lessonPageEditorSlice';
|
||||||
lessonContents,
|
import { useEffect } from 'react';
|
||||||
lessonThumbnail,
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
setCurrentLessonId,
|
import { Card, Flex } from 'antd';
|
||||||
setEditorActive,
|
import { Constants } from 'core/utils/utils';
|
||||||
setLessonContents,
|
import HeaderBar from 'core/components/Header';
|
||||||
setLessonState,
|
import Droppable from './Droppable';
|
||||||
} from "./lessonPageEditorSlice";
|
import LessonPreviewCard from 'shared/components/MyLessonPreviewCard';
|
||||||
import { useEffect } from "react";
|
import { useGetLessonContentsQuery, useGetLessonSettingsQuery, useUpdateLessonPreviewTitleMutation } from 'core/services/lessons';
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import MyErrorResult from 'shared/components/MyResult';
|
||||||
import { Card, Flex } from "antd";
|
import styles from './styles.module.css';
|
||||||
import { Constants } from "core/utils/utils";
|
import MyEmpty from 'shared/components/MyEmpty';
|
||||||
import HeaderBar from "core/components/Header";
|
|
||||||
import Droppable from "./Droppable";
|
|
||||||
import LessonPreviewCard from "shared/components/MyLessonPreviewCard";
|
|
||||||
import {
|
|
||||||
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 PreviewCard: React.FC = () => {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const { lessonId } = useParams();
|
const { lessonId } = useParams();
|
||||||
|
|
||||||
const { data, error, isLoading, refetch } = useGetLessonSettingsQuery(
|
const { data, error, isLoading, refetch } = useGetLessonSettingsQuery(lessonId as string, {
|
||||||
lessonId as string,
|
|
||||||
{
|
|
||||||
refetchOnMountOrArgChange: true,
|
refetchOnMountOrArgChange: true,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const [updateLessonPreviewTitle] = useUpdateLessonPreviewTitleMutation();
|
const [updateLessonPreviewTitle] = useUpdateLessonPreviewTitleMutation();
|
||||||
|
|
||||||
|
@ -48,8 +34,8 @@ const PreviewCard: React.FC = () => {
|
||||||
lessonId={lessonId as string}
|
lessonId={lessonId as string}
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
lessonSettings={{
|
lessonSettings={{
|
||||||
Title: data?.Title || "",
|
Title: data?.Title || '',
|
||||||
ThumbnailUrl: data?.ThumbnailUrl || "",
|
ThumbnailUrl: data?.ThumbnailUrl || '',
|
||||||
}}
|
}}
|
||||||
onEditTitle={async (newTitle) => {
|
onEditTitle={async (newTitle) => {
|
||||||
try {
|
try {
|
||||||
|
@ -74,12 +60,9 @@ const LessonContentComponents: React.FC = () => {
|
||||||
const { lessonId } = useParams();
|
const { lessonId } = useParams();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const { data, error, isLoading } = useGetLessonContentsQuery(
|
const { data, error, isLoading } = useGetLessonContentsQuery(lessonId as string, {
|
||||||
lessonId as string,
|
|
||||||
{
|
|
||||||
refetchOnMountOrArgChange: true,
|
refetchOnMountOrArgChange: true,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const lnContents = useSelector(lessonContents);
|
const lnContents = useSelector(lessonContents);
|
||||||
|
|
||||||
|
@ -94,11 +77,7 @@ const LessonContentComponents: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<Card loading={isLoading}>
|
<Card loading={isLoading}>
|
||||||
<Flex vertical gap={16}>
|
<Flex vertical gap={16}>
|
||||||
{!lnContents || lnContents.length == 0 ? (
|
{!lnContents || lnContents.length == 0 ? <MyEmpty /> : <Droppable items={lnContents} />}
|
||||||
<MyEmpty />
|
|
||||||
) : (
|
|
||||||
<Droppable items={lnContents} />
|
|
||||||
)}
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -125,27 +104,12 @@ export default function LessonPageEditor() {
|
||||||
<>
|
<>
|
||||||
<HeaderBar
|
<HeaderBar
|
||||||
theme="light"
|
theme="light"
|
||||||
backTo={Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(
|
backTo={Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(':lessonId', lessonId as string)}
|
||||||
":lessonId",
|
onView={() => navigate(Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(':lessonId', lessonId as string))}
|
||||||
lessonId as string
|
|
||||||
)}
|
|
||||||
onView={() =>
|
|
||||||
navigate(
|
|
||||||
Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(
|
|
||||||
":lessonId",
|
|
||||||
lessonId as string
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Flex justify="center" style={{ paddingTop: 24 }}>
|
<Flex justify="center" style={{ paddingTop: 24 }}>
|
||||||
<Flex
|
<Flex justify="center" vertical gap={16} className={styles.cardContainer}>
|
||||||
justify="center"
|
|
||||||
vertical
|
|
||||||
gap={16}
|
|
||||||
className={styles.cardContainer}
|
|
||||||
>
|
|
||||||
<PreviewCard />
|
<PreviewCard />
|
||||||
|
|
||||||
<LessonContentComponents />
|
<LessonContentComponents />
|
||||||
|
|
|
@ -0,0 +1,296 @@
|
||||||
|
import { DownOutlined, HeartFilled, HeartOutlined } from '@ant-design/icons';
|
||||||
|
import { Avatar, Button, Card, Collapse, Divider, Flex, Form, Input, InputRef, Typography } from 'antd';
|
||||||
|
import Meta from 'antd/es/card/Meta';
|
||||||
|
import TextArea from 'antd/es/input/TextArea';
|
||||||
|
import { LessonQuestion, LessonQuestionReply } from 'core/types/lesson';
|
||||||
|
import { Constants } from 'core/utils/utils';
|
||||||
|
import React, { useRef } from 'react';
|
||||||
|
|
||||||
|
export default function Questions({ lessionID }: { lessionID: string }) {
|
||||||
|
let questions: LessonQuestion[] = [
|
||||||
|
{
|
||||||
|
Id: '1',
|
||||||
|
LessionId: '1',
|
||||||
|
Question: 'What is the capital of Germany?',
|
||||||
|
Likes: 5,
|
||||||
|
CreatorUserId: '1',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: '2',
|
||||||
|
LessionId: '1',
|
||||||
|
Question: 'What is the capital of France?',
|
||||||
|
Likes: 3,
|
||||||
|
CreatorUserId: '2',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: '3',
|
||||||
|
LessionId: '1',
|
||||||
|
Question: 'What is the capital of Italy?',
|
||||||
|
Likes: 2,
|
||||||
|
CreatorUserId: '3',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex justify="center">
|
||||||
|
<Flex style={{ width: 800, maxWidth: 800 * 0.9 }} vertical>
|
||||||
|
<Typography.Title level={3}>Questions</Typography.Title>
|
||||||
|
<Form layout="vertical">
|
||||||
|
<Form.Item label="Ask a question">
|
||||||
|
<Input.TextArea placeholder={'Type something'} />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary">Submit</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
<Flex vertical style={{}}>
|
||||||
|
{questions.map((question) => (
|
||||||
|
<QuestionItem key={question.Id} question={question} />
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type HandleReplyFunction = (text: string, replyID?: string) => Promise<void>;
|
||||||
|
|
||||||
|
export function QuestionItem({ question }: { question: LessonQuestion }) {
|
||||||
|
const [showReplies, setShowReplies] = React.useState(1);
|
||||||
|
|
||||||
|
let user = {
|
||||||
|
Id: '132154153613',
|
||||||
|
FirstName: 'Anja',
|
||||||
|
LastName: 'Blasinstroment',
|
||||||
|
};
|
||||||
|
|
||||||
|
let questionsReplys: LessonQuestionReply[] = [
|
||||||
|
{
|
||||||
|
Id: '1',
|
||||||
|
QuestionId: '1',
|
||||||
|
Reply: 'Berlin',
|
||||||
|
Likes: 5,
|
||||||
|
CreatorUserId: '1',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: '2',
|
||||||
|
QuestionId: '1',
|
||||||
|
Reply: 'Munich',
|
||||||
|
Likes: 3,
|
||||||
|
CreatorUserId: '2',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: '3',
|
||||||
|
QuestionId: '1',
|
||||||
|
Reply: 'Hamburg',
|
||||||
|
Likes: 2,
|
||||||
|
CreatorUserId: '3',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: '4',
|
||||||
|
QuestionId: '1',
|
||||||
|
Reply: 'Cologne',
|
||||||
|
Likes: 0,
|
||||||
|
CreatorUserId: '3',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: '5',
|
||||||
|
QuestionId: '1',
|
||||||
|
Reply: 'Frankfurt',
|
||||||
|
Likes: 0,
|
||||||
|
CreatorUserId: '3',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: '6',
|
||||||
|
QuestionId: '1',
|
||||||
|
Reply: 'Stuttgart',
|
||||||
|
Likes: 2,
|
||||||
|
CreatorUserId: '3',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: '7',
|
||||||
|
QuestionId: '1',
|
||||||
|
Reply: 'Düsseldorf',
|
||||||
|
Likes: 10,
|
||||||
|
CreatorUserId: '3',
|
||||||
|
CreatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
UpdatedAt: '2021-09-01T12:00:00Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
async function handleReply(text: string, replyID?: string) {
|
||||||
|
console.log('reply', text);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QuestionUIRaw
|
||||||
|
userID={user.Id}
|
||||||
|
text={question.Question}
|
||||||
|
childContent={
|
||||||
|
<div>
|
||||||
|
{(() => {
|
||||||
|
let nodes = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < questionsReplys.length; i++) {
|
||||||
|
if (i > showReplies - 1) {
|
||||||
|
nodes.push(
|
||||||
|
<Button key="showMore" type="link" color="primary" onClick={() => setShowReplies(showReplies + 3)} style={{ marginLeft: 64 }}>
|
||||||
|
Show more
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push(<QuestionReplyItem key={'reply_' + questionsReplys[i].Id} question={questionsReplys[i]} handleReply={handleReply} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
likes={question.Likes}
|
||||||
|
onReply={handleReply}
|
||||||
|
onLike={() => {}}
|
||||||
|
replyID={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuestionReplyItem({ question, handleReply }: { question: LessonQuestionReply; handleReply: HandleReplyFunction }) {
|
||||||
|
let user = {
|
||||||
|
Id: '132154153613',
|
||||||
|
FirstName: 'Anja',
|
||||||
|
LastName: 'Blasinstroment',
|
||||||
|
};
|
||||||
|
|
||||||
|
return <QuestionUIRaw userID={user.Id} text={question.Reply} childContent={<></>} likes={question.Likes} onReply={handleReply} onLike={() => {}} replyID={question.Id} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuestionUIRaw({
|
||||||
|
userID,
|
||||||
|
text,
|
||||||
|
childContent,
|
||||||
|
likes,
|
||||||
|
replyID,
|
||||||
|
onReply,
|
||||||
|
onLike,
|
||||||
|
}: {
|
||||||
|
userID: string;
|
||||||
|
text: string;
|
||||||
|
childContent: React.ReactNode;
|
||||||
|
likes: number;
|
||||||
|
replyID?: string;
|
||||||
|
onReply: HandleReplyFunction;
|
||||||
|
onLike: () => void;
|
||||||
|
}) {
|
||||||
|
const [hasLiked, setHasLiked] = React.useState(false);
|
||||||
|
|
||||||
|
const [replyFormVisible, setReplyFormVisible] = React.useState(false);
|
||||||
|
const [replyText, setReplyText] = React.useState<null | string>(null);
|
||||||
|
const [isSendingReply, setIsSendingReply] = React.useState(false);
|
||||||
|
|
||||||
|
let user = {
|
||||||
|
Id: '132154153613',
|
||||||
|
FirstName: 'Anja',
|
||||||
|
LastName: 'Blasinstroment',
|
||||||
|
};
|
||||||
|
|
||||||
|
const userAt = `@${user.FirstName} ${user.LastName} `;
|
||||||
|
|
||||||
|
async function toggleLike() {
|
||||||
|
setHasLiked(!hasLiked);
|
||||||
|
}
|
||||||
|
|
||||||
|
// useref to focus on the input field
|
||||||
|
const inputRef = useRef<InputRef>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Flex gap={16}>
|
||||||
|
<Avatar src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`} size={56} />
|
||||||
|
<Flex vertical style={{ width: '100%' }}>
|
||||||
|
<Typography style={{ fontSize: 24, fontWeight: 800 }}>
|
||||||
|
{user.FirstName} {user.LastName}
|
||||||
|
</Typography>
|
||||||
|
<Typography style={{ fontSize: 18, fontWeight: 500 }}>{text}</Typography>
|
||||||
|
<Flex gap={0} align="center">
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={hasLiked ? <HeartFilled /> : <HeartOutlined />}
|
||||||
|
shape="circle"
|
||||||
|
size="large"
|
||||||
|
style={{ color: hasLiked ? 'red' : undefined, transform: hasLiked ? 'scale(1.2)' : 'scale(1)', transition: 'all 0.3s ease-in-out' }}
|
||||||
|
onClick={toggleLike}
|
||||||
|
></Button>
|
||||||
|
|
||||||
|
<Typography style={{ fontSize: 16, fontWeight: 400, pointerEvents: 'none' }}>{likes >= 1 ? likes : ' '}</Typography>
|
||||||
|
<Button
|
||||||
|
type={replyFormVisible ? 'link' : 'text'}
|
||||||
|
onClick={() => {
|
||||||
|
if (replyText === null) setReplyText(userAt);
|
||||||
|
setReplyFormVisible(!replyFormVisible);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (inputRef.current) {
|
||||||
|
const input = inputRef.current;
|
||||||
|
input.focus({ cursor: 'end' });
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{replyFormVisible ? 'Hide' : 'Reply'}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
{replyFormVisible ? (
|
||||||
|
<Form
|
||||||
|
disabled={isSendingReply}
|
||||||
|
onFinish={async () => {
|
||||||
|
setIsSendingReply(true);
|
||||||
|
await onReply(replyText ? replyText : '', replyID);
|
||||||
|
|
||||||
|
setIsSendingReply(false);
|
||||||
|
setReplyFormVisible(false);
|
||||||
|
setReplyText(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Form.Item name="reply" rules={[{ required: true, message: 'Please write a reply' }]}>
|
||||||
|
<Input.TextArea
|
||||||
|
ref={inputRef}
|
||||||
|
defaultValue={replyText ? replyText : userAt}
|
||||||
|
value={replyText ? replyText : userAt}
|
||||||
|
placeholder="Write a reply"
|
||||||
|
onChange={(e) => setReplyText(e.target.value)}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item>
|
||||||
|
<Button type="primary" loading={isSendingReply} htmlType="submit">
|
||||||
|
Reply
|
||||||
|
</Button>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
) : null}
|
||||||
|
{childContent}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import { LessonContent, LessonState } from 'core/types/lesson';
|
||||||
|
|
||||||
|
interface AddLessonContentAction {
|
||||||
|
type: string;
|
||||||
|
payload: LessonContent;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
export const lessonPageEditorSlice = createSlice({
|
||||||
|
name: 'lessonQuestions',
|
||||||
|
initialState: {
|
||||||
|
editorActive: false,
|
||||||
|
currentLessonId: '', // required in sideMenu because has no access to useParams
|
||||||
|
lessonThumbnail: {
|
||||||
|
img: '',
|
||||||
|
title: 'Tesdt',
|
||||||
|
},
|
||||||
|
lessonContents: [] as LessonContent[],
|
||||||
|
lessonState: LessonState.Draft,
|
||||||
|
},
|
||||||
|
reducers: {
|
||||||
|
setEditorActive: (state, action) => {
|
||||||
|
state.editorActive = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
editorActive: (state) => state.editorActive,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const { setEditorActive } = lessonPageEditorSlice.actions;
|
||||||
|
|
||||||
|
export const { editorActive } = lessonPageEditorSlice.selectors;
|
||||||
|
*/
|
|
@ -17,48 +17,49 @@ export type Component = {
|
||||||
|
|
||||||
const componentsGroups: ComponentGroup[] = [
|
const componentsGroups: ComponentGroup[] = [
|
||||||
{
|
{
|
||||||
category: "Common",
|
category: 'Common',
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: 0,
|
type: 0,
|
||||||
name: "Header",
|
name: 'Header',
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_header.svg",
|
thumbnail: '/editor/thumbnails/component_thumbnail_header.svg',
|
||||||
invertThumbnailAtDarkmode: true,
|
invertThumbnailAtDarkmode: true,
|
||||||
defaultData: "Header",
|
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",
|
defaultData: 'Text',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
category: "Media",
|
category: 'Media',
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
type: 2,
|
type: 2,
|
||||||
name: "Image",
|
name: 'Image',
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_image.png",
|
thumbnail: '/editor/thumbnails/component_thumbnail_image.png',
|
||||||
uploadImage: true,
|
uploadImage: true,
|
||||||
uploadFileTypes: ["image/*"],
|
uploadFileTypes: ['image/*'],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 3,
|
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: 4,
|
type: 4,
|
||||||
name: "Video",
|
name: 'Video',
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
thumbnail: '/editor/thumbnails/component_thumbnail_youtube.png',
|
||||||
invertThumbnailAtDarkmode: true,
|
invertThumbnailAtDarkmode: true,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
{
|
{
|
||||||
category: "HTML",
|
category: "HTML",
|
||||||
components: [
|
components: [
|
||||||
|
@ -79,7 +80,7 @@ const componentsGroups: ComponentGroup[] = [
|
||||||
thumbnail: "/editor/thumbnails/component_thumbnail_banner.png",
|
thumbnail: "/editor/thumbnails/component_thumbnail_banner.png",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
}, */
|
||||||
];
|
];
|
||||||
|
|
||||||
const componentsMap: { [key: string]: Component } = (() => {
|
const componentsMap: { [key: string]: Component } = (() => {
|
||||||
|
|
|
@ -1,21 +1,25 @@
|
||||||
import { LessonContent } from "core/types/lesson";
|
import { LessonContent } from 'core/types/lesson';
|
||||||
import { getTypeByName } from "./components";
|
import { getTypeByName } from './components';
|
||||||
import { Button, Input, Typography } from "antd";
|
import { Button, Input, Typography, Flex } from 'antd';
|
||||||
import { useUpdateLessonContentMutation } from "core/services/lessons";
|
import { useUpdateLessonContentMutation } from 'core/services/lessons';
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from 'react-redux';
|
||||||
import { currentLessonId } from "./LessonPageEditor/lessonPageEditorSlice";
|
import { currentLessonId } from './LessonPageEditor/lessonPageEditorSlice';
|
||||||
import { useRef } from "react";
|
import { useRef, useEffect } from 'react';
|
||||||
import MyUpload from "shared/components/MyUpload";
|
import MyUpload from 'shared/components/MyUpload';
|
||||||
|
import { Constants } from 'core/utils/utils';
|
||||||
|
import { MediaPlayer, MediaProvider } from '@vidstack/react';
|
||||||
|
import { defaultLayoutIcons, DefaultVideoLayout } from '@vidstack/react/player/layouts/default';
|
||||||
|
import '@vidstack/react/player/styles/default/theme.css';
|
||||||
|
import '@vidstack/react/player/styles/default/layouts/video.css';
|
||||||
|
|
||||||
export function Converter({
|
const extractVideoId = (url: string) => {
|
||||||
mode,
|
// regex to extract video id from youtube url
|
||||||
lessonContent,
|
const regex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/(?:watch\?v=|embed\/|v\/|.+\?v=|.+\/|.+v=)?([^"&?\/\s]{11})/;
|
||||||
onEdit,
|
const match = url.match(regex);
|
||||||
}: {
|
return match ? match[1] : url;
|
||||||
mode: "view" | "edititable";
|
};
|
||||||
lessonContent: LessonContent;
|
|
||||||
onEdit?: (newData: string) => void;
|
export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edititable'; lessonContent: LessonContent; onEdit?: (newData: string) => void }) {
|
||||||
}) {
|
|
||||||
const lessonId = useSelector(currentLessonId);
|
const lessonId = useSelector(currentLessonId);
|
||||||
|
|
||||||
const [reqUpdateLessonContent] = useUpdateLessonContentMutation();
|
const [reqUpdateLessonContent] = useUpdateLessonContentMutation();
|
||||||
|
@ -23,21 +27,15 @@ export function Converter({
|
||||||
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
||||||
|
|
||||||
switch (lessonContent.Type) {
|
switch (lessonContent.Type) {
|
||||||
case getTypeByName("Header"):
|
case getTypeByName('Header'):
|
||||||
if (mode === "view") {
|
if (mode === 'view') {
|
||||||
return (
|
return <div style={{ fontWeight: 'bold', fontSize: 24, wordBreak: 'break-all' }}>{lessonContent.Data}</div>;
|
||||||
<div
|
|
||||||
style={{ fontWeight: "bold", fontSize: 24, wordBreak: "break-all" }}
|
|
||||||
>
|
|
||||||
{lessonContent.Data}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Typography.Title
|
<Typography.Title
|
||||||
editable={{
|
editable={{
|
||||||
triggerType: "text" as any,
|
triggerType: 'text' as any,
|
||||||
onChange: (event) => {
|
onChange: (event) => {
|
||||||
onEdit?.(event);
|
onEdit?.(event);
|
||||||
|
|
||||||
|
@ -55,29 +53,28 @@ export function Converter({
|
||||||
level={1}
|
level={1}
|
||||||
style={{
|
style={{
|
||||||
margin: 0,
|
margin: 0,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{lessonContent.Data}
|
{lessonContent.Data}
|
||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
);
|
);
|
||||||
case getTypeByName("Text"):
|
case getTypeByName('Text'):
|
||||||
if (mode === "view") {
|
if (mode === 'view') {
|
||||||
return (
|
const formattedText = lessonContent.Data.split('\n').map((line, index) => <li key={index}>{line || '\u00A0'}</li>);
|
||||||
<div style={{ fontSize: 16, wordBreak: "break-all" }}>
|
|
||||||
{lessonContent.Data}
|
return <ul style={{ fontSize: 16, wordBreak: 'break-all', padding: 0, margin: 0, listStyleType: 'none' }}>{formattedText}</ul>;
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Input.TextArea
|
<Input.TextArea
|
||||||
|
autoSize
|
||||||
variant="borderless"
|
variant="borderless"
|
||||||
placeholder="Input text here"
|
placeholder="Input text here"
|
||||||
style={{ width: "100%" }}
|
style={{ width: '100%', padding: 0, paddingTop: 4 }}
|
||||||
value={lessonContent.Data}
|
value={lessonContent.Data}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
console.log("edit");
|
console.log('edit');
|
||||||
|
|
||||||
onEdit?.(event.target.value);
|
onEdit?.(event.target.value);
|
||||||
|
|
||||||
|
@ -99,26 +96,26 @@ export function Converter({
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case getTypeByName("Image"):
|
case getTypeByName('Image'):
|
||||||
console.log("image", lessonContent.Data);
|
console.log('image', lessonContent.Data);
|
||||||
|
|
||||||
if (mode === "view" && lessonContent.Data === "") {
|
if (mode === 'view' && lessonContent.Data === '') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
height: 120,
|
height: 120,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
backgroundColor: "#EBEBEB",
|
backgroundColor: '#EBEBEB',
|
||||||
marginRight: 8,
|
marginRight: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: "50%",
|
top: '50%',
|
||||||
left: "50%",
|
left: '50%',
|
||||||
transform: "translate(-50%, -50%)",
|
transform: 'translate(-50%, -50%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
No image provided
|
No image provided
|
||||||
|
@ -127,55 +124,216 @@ export function Converter({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lessonContent.Data === "") {
|
const GalleryUpload = () => {
|
||||||
|
return (
|
||||||
|
<MyUpload
|
||||||
|
action={`/lessons/${lessonId}/contents/${lessonContent.Id}/file/image`}
|
||||||
|
onChange={(info) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
console.log('done');
|
||||||
|
onEdit?.(info.file.response.Data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
imgCropProps={{
|
||||||
|
aspect: 5 / 4,
|
||||||
|
children: <></>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button type="link">Gallery</Button>
|
||||||
|
</MyUpload>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lessonContent.Data === '') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: "relative",
|
position: 'relative',
|
||||||
height: 120,
|
height: 120,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
backgroundColor: "#EBEBEB",
|
backgroundColor: '#EBEBEB',
|
||||||
margin: "12px 12px 12px 0",
|
margin: '12px 12px 12px 0',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
position: "absolute",
|
position: 'absolute',
|
||||||
top: "50%",
|
top: '50%',
|
||||||
left: "50%",
|
left: '50%',
|
||||||
transform: "translate(-50%, -50%)",
|
transform: 'translate(-50%, -50%)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span>Choose image from</span>
|
<span>Choose image from</span>
|
||||||
|
|
||||||
<MyUpload>
|
<GalleryUpload />
|
||||||
<Button type="link">Gallery</Button>
|
|
||||||
</MyUpload>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Flex vertical style={{ width: '100%' }}>
|
||||||
<img src={lessonContent.Data} alt="img" style={{ width: "100%" }} />
|
<img src={`${Constants.STATIC_CONTENT_ADDRESS}/${lessonContent.Data}`} alt="img" style={{ width: '100%' }} />
|
||||||
</>
|
|
||||||
|
{mode === 'edititable' && (
|
||||||
|
<Flex style={{ width: '100%', backgroundColor: '#EBEBEB' }} justify="center">
|
||||||
|
<div>
|
||||||
|
<span>Choose another image from</span>
|
||||||
|
<GalleryUpload />
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
case getTypeByName("YouTube"):
|
case getTypeByName('YouTube'):
|
||||||
|
const videoId = extractVideoId(lessonContent.Data);
|
||||||
|
|
||||||
|
console.log('videoId', videoId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ fontWeight: "700", fontSize: 20, width: "100%" }}>
|
<Flex vertical style={{ width: '100%', paddingBottom: 12 }}>
|
||||||
{lessonContent.Data}
|
<iframe
|
||||||
|
width="100%"
|
||||||
|
height={mode === 'view' ? 422 : 390}
|
||||||
|
style={{ border: 0 }}
|
||||||
|
src={`https://www.youtube.com/embed/${videoId}`}
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||||
|
></iframe>
|
||||||
|
|
||||||
|
{mode === 'edititable' && (
|
||||||
|
<>
|
||||||
|
<Typography.Text>Video ID</Typography.Text>
|
||||||
|
<Input
|
||||||
|
placeholder="https://www.youtube.com/watch?v=Robzc9p1l50 or Robzc9p1l50"
|
||||||
|
value={lessonContent.Data}
|
||||||
|
onChange={(event) => {
|
||||||
|
console.warn('edit', event.target.value, videoId);
|
||||||
|
|
||||||
|
onEdit?.(event.target.value);
|
||||||
|
|
||||||
|
if (debounceRef.current) {
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.target.value === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
try {
|
||||||
|
reqUpdateLessonContent({
|
||||||
|
lessonId: lessonId,
|
||||||
|
contentId: lessonContent.Id,
|
||||||
|
data: extractVideoId(event.target.value),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
case getTypeByName('Video'):
|
||||||
|
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 video provided
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case getTypeByName("Video"):
|
}
|
||||||
return <div style={{ fontSize: 14, width: "100%" }}>Not implemented</div>;
|
|
||||||
case getTypeByName("Iframe"):
|
const VideoUpload = () => {
|
||||||
return <div style={{ fontSize: 14, width: "100%" }}>Not implemented</div>;
|
return (
|
||||||
case getTypeByName("Banner"):
|
<MyUpload
|
||||||
return <div style={{ fontSize: 14, width: "100%" }}>Not implemented</div>;
|
action={`/lessons/${lessonId}/contents/${lessonContent.Id}/file/video`}
|
||||||
|
onChange={(info) => {
|
||||||
|
if (info.file.status === 'done') {
|
||||||
|
console.log('done');
|
||||||
|
onEdit?.(info.file.response.Data);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
accept={Constants.ACCEPTED_VIDEO_FILE_TYPES}
|
||||||
|
>
|
||||||
|
<Button type="link">Video</Button>
|
||||||
|
</MyUpload>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 video from</span>
|
||||||
|
|
||||||
|
<VideoUpload />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex vertical style={{ width: '100%' }}>
|
||||||
|
<MediaPlayer load="idle" src={`${Constants.STATIC_CONTENT_ADDRESS}/${lessonContent.Data}`}>
|
||||||
|
<MediaProvider />
|
||||||
|
<DefaultVideoLayout icons={defaultLayoutIcons} />
|
||||||
|
</MediaPlayer>
|
||||||
|
|
||||||
|
{mode === 'edititable' && (
|
||||||
|
<Flex style={{ width: '100%', backgroundColor: '#EBEBEB', height: 48 }} justify="center" align="center">
|
||||||
|
<div>
|
||||||
|
<span>Choose another video from</span>
|
||||||
|
<VideoUpload />
|
||||||
|
</div>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
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:
|
default:
|
||||||
return <div>Unknown type</div>;
|
return <div>Unknown type</div>;
|
||||||
|
|
|
@ -6,27 +6,60 @@ import { SaveOutlined } from "@ant-design/icons";
|
||||||
import MyUpload from "shared/components/MyUpload";
|
import MyUpload from "shared/components/MyUpload";
|
||||||
import { Constants } from "core/utils/utils";
|
import { Constants } from "core/utils/utils";
|
||||||
import ColorPicker from "antd/es/color-picker";
|
import ColorPicker from "antd/es/color-picker";
|
||||||
|
import { useGetOrganizationSettingsQuery } from "core/services/organization";
|
||||||
|
import MyErrorResult from "shared/components/MyResult";
|
||||||
|
import { useForm } from "antd/es/form/Form";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { AggregationColor } from "antd/es/color-picker/color";
|
||||||
|
|
||||||
|
type FieldType = {
|
||||||
|
primaryColor: string;
|
||||||
|
companyName: string;
|
||||||
|
subdomain: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
|
const { data, error, isLoading } = useGetOrganizationSettingsQuery(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
refetchOnMountOrArgChange: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const [form] = useForm<FieldType>();
|
||||||
|
|
||||||
|
const handleSave = (values: FieldType) => {
|
||||||
|
console.log(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
primaryColor: data.PrimaryColor,
|
||||||
|
companyName: data.CompanyName,
|
||||||
|
subdomain: data.Subdomain,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
if (error) return <MyErrorResult />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||||
|
|
||||||
<MyContainer>
|
<MyContainer>
|
||||||
<Flex vertical gap={16} style={{ paddingBottom: 16 }}>
|
<Flex vertical gap={16} style={{ paddingBottom: 16 }}>
|
||||||
|
<Form form={form} onFinish={handleSave}>
|
||||||
<Card
|
<Card
|
||||||
|
loading={isLoading}
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
title="Branding"
|
||||||
<Flex vertical gap={2}>
|
extra={
|
||||||
<Form>
|
|
||||||
<Flex justify="space-between" align="center">
|
|
||||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
|
||||||
Branding
|
|
||||||
</Typography.Title>
|
|
||||||
<Button
|
<Button
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -34,9 +67,9 @@ export default function Settings() {
|
||||||
size="large"
|
size="large"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
/>
|
/>
|
||||||
</Flex>
|
}
|
||||||
<Divider style={{ margin: 0, padding: 0 }} />
|
>
|
||||||
|
<Flex vertical gap={2}>
|
||||||
<Flex gap={32}>
|
<Flex gap={32}>
|
||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
<Typography.Text style={{ fontSize: 16 }}>
|
<Typography.Text style={{ fontSize: 16 }}>
|
||||||
|
@ -47,6 +80,7 @@ export default function Settings() {
|
||||||
defaultValue="#1677ff"
|
defaultValue="#1677ff"
|
||||||
size="small"
|
size="small"
|
||||||
showText
|
showText
|
||||||
|
format="hex"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
@ -63,7 +97,11 @@ export default function Settings() {
|
||||||
Subdomain
|
Subdomain
|
||||||
</Typography.Text>
|
</Typography.Text>
|
||||||
<Form.Item name="subdomain">
|
<Form.Item name="subdomain">
|
||||||
<Input addonBefore="https://" addonAfter=". jannex . de" defaultValue="mysite" />
|
<Input
|
||||||
|
addonBefore="https://"
|
||||||
|
addonAfter=". jannex.de"
|
||||||
|
defaultValue="mysite"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex vertical>
|
<Flex vertical>
|
||||||
|
@ -128,9 +166,9 @@ export default function Settings() {
|
||||||
/>
|
/>
|
||||||
</MyUpload>
|
</MyUpload>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Form>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Form>
|
||||||
</Flex>
|
</Flex>
|
||||||
</MyContainer>
|
</MyContainer>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import MyTable from "shared/components/MyTable";
|
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 { Button, Flex } from 'antd';
|
||||||
import { UserAddOutlined } from "@ant-design/icons";
|
import { UserAddOutlined } from '@ant-design/icons';
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from 'react-router-dom';
|
||||||
import { Constants } from "core/utils/utils";
|
import { Constants } from 'core/utils/utils';
|
||||||
import { useGetTeamQuery } from "core/services/team";
|
import { useGetTeamQuery } from 'core/services/organization';
|
||||||
import MyErrorResult from "shared/components/MyResult";
|
import MyErrorResult from 'shared/components/MyResult';
|
||||||
|
|
||||||
const TeamList: React.FC = () => {
|
const TeamList: React.FC = () => {
|
||||||
const { data, error, isLoading } = useGetTeamQuery(undefined, {
|
const { data, error, isLoading } = useGetTeamQuery(undefined, {
|
||||||
|
@ -17,34 +17,34 @@ const TeamList: React.FC = () => {
|
||||||
const getTableContent = () => {
|
const getTableContent = () => {
|
||||||
let items = [
|
let items = [
|
||||||
{
|
{
|
||||||
title: "First name",
|
title: 'First name',
|
||||||
dataIndex: "firstName",
|
dataIndex: 'firstName',
|
||||||
key: "firstName",
|
key: 'firstName',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Last name",
|
title: 'Last name',
|
||||||
dataIndex: "lastName",
|
dataIndex: 'lastName',
|
||||||
key: "lastName",
|
key: 'lastName',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Email",
|
title: 'Email',
|
||||||
dataIndex: "email",
|
dataIndex: 'email',
|
||||||
key: "email",
|
key: 'email',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Role",
|
title: 'Role',
|
||||||
dataIndex: "role",
|
dataIndex: 'role',
|
||||||
key: "role",
|
key: 'role',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Status",
|
title: 'Status',
|
||||||
dataIndex: "status",
|
dataIndex: 'status',
|
||||||
key: "status",
|
key: 'status',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Actions",
|
title: 'Actions',
|
||||||
dataIndex: "actions",
|
dataIndex: 'actions',
|
||||||
key: "actions",
|
key: 'actions',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -68,13 +68,11 @@ const TeamList: React.FC = () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return items;
|
return items;
|
||||||
}
|
};
|
||||||
|
|
||||||
if (error) return <MyErrorResult />;
|
if (error) return <MyErrorResult />;
|
||||||
|
|
||||||
return (
|
return <MyTable loading={isLoading} columns={getTableContent()} dataSource={getTableItems()} pagination={false} />;
|
||||||
<MyTable loading={isLoading} columns={getTableContent()} dataSource={getTableItems()} pagination={false} />
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Team() {
|
export default function Team() {
|
||||||
|
@ -84,8 +82,8 @@ export default function Team() {
|
||||||
|
|
||||||
<MyContainer
|
<MyContainer
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
flexDirection: "column",
|
flexDirection: 'column',
|
||||||
gap: 16,
|
gap: 16,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Card, CardProps, Flex } from "antd";
|
import { Card, CardProps, Flex } from 'antd';
|
||||||
import { MyContainer } from "../MyContainer";
|
import { MyContainer } from '../MyContainer';
|
||||||
|
|
||||||
interface MyMiddleCardProps extends CardProps {}
|
interface MyMiddleCardProps extends CardProps {
|
||||||
|
outOfCardChildren?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
const MyMiddleCard: React.FC<MyMiddleCardProps> = ({ children,
|
const MyMiddleCard: React.FC<MyMiddleCardProps> = ({ children, outOfCardChildren, ...props }) => {
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<MyContainer>
|
<MyContainer>
|
||||||
<Flex justify="center">
|
<Flex justify="center">
|
||||||
|
@ -19,8 +19,9 @@ const MyMiddleCard: React.FC<MyMiddleCardProps> = ({ children,
|
||||||
{children}
|
{children}
|
||||||
</Card>
|
</Card>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
{outOfCardChildren}
|
||||||
</MyContainer>
|
</MyContainer>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default MyMiddleCard;
|
export default MyMiddleCard;
|
|
@ -1,58 +1,67 @@
|
||||||
import ImgCrop, { ImgCropProps } from "antd-img-crop";
|
import ImgCrop, { ImgCropProps } from 'antd-img-crop';
|
||||||
import Upload from "antd/es/upload/Upload";
|
import Upload from 'antd/es/upload/Upload';
|
||||||
import { getApiHeader } from "core/helper/api";
|
import { getApiHeader } from 'core/helper/api';
|
||||||
import { Constants } from "core/utils/utils";
|
import { Constants } from 'core/utils/utils';
|
||||||
|
import { Fragment } from 'react';
|
||||||
|
|
||||||
export default function MyUpload({
|
export default function MyUpload({
|
||||||
children,
|
children,
|
||||||
imgCropProps,
|
imgCropProps,
|
||||||
accept = Constants.ACCEPTED_IMAGE_FILE_TYPES.join(","),
|
accept = Constants.ACCEPTED_IMAGE_FILE_TYPES,
|
||||||
maxCount = 1,
|
maxCount = 1,
|
||||||
showUploadList = false,
|
showUploadList = false,
|
||||||
headers = getApiHeader(),
|
headers = getApiHeader(),
|
||||||
action,
|
action,
|
||||||
onChange,
|
onChange,
|
||||||
|
fileType = 'image',
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
imgCropProps?: ImgCropProps;
|
imgCropProps?: ImgCropProps;
|
||||||
accept?: string;
|
accept?: string | string[];
|
||||||
maxCount?: number;
|
maxCount?: number;
|
||||||
showUploadList?: boolean;
|
showUploadList?: boolean;
|
||||||
headers?: any;
|
headers?: any;
|
||||||
action?: string;
|
action?: string;
|
||||||
onChange?: (info: any) => void;
|
onChange?: (info: any) => void;
|
||||||
|
fileType?: 'image' | 'video';
|
||||||
}) {
|
}) {
|
||||||
const beforeUpload = (file: File) => {
|
const beforeUpload = (file: File) => {
|
||||||
if (!Constants.ACCEPTED_IMAGE_FILE_TYPES.includes(file.type)) {
|
if (!accept.includes(file.type)) {
|
||||||
console.error("File typ not allowed!");
|
console.error('File typ not allowed!');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.size > Constants.MAX_IMAGE_SIZE) {
|
if (file.size > Constants.MAX_IMAGE_SIZE) {
|
||||||
console.error("Image is to large!");
|
console.error('Image is to large!');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const acceptFileTypes = Array.isArray(accept) ? accept.join(',') : (accept as string);
|
||||||
<ImgCrop
|
|
||||||
{...imgCropProps}
|
const MyUpload = () => (
|
||||||
rotationSlider
|
|
||||||
>
|
|
||||||
<Upload
|
<Upload
|
||||||
accept={accept}
|
accept={acceptFileTypes}
|
||||||
maxCount={maxCount}
|
maxCount={maxCount}
|
||||||
showUploadList={showUploadList}
|
showUploadList={showUploadList}
|
||||||
headers={headers}
|
headers={headers}
|
||||||
action={`${Constants.API_ADDRESS}${action}`}
|
action={`${Constants.API_ADDRESS}${action}`}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
beforeUpload={beforeUpload}
|
beforeUpload={beforeUpload}
|
||||||
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Upload>
|
</Upload>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (fileType === 'video') {
|
||||||
|
return <MyUpload />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ImgCrop {...imgCropProps} rotationSlider>
|
||||||
|
<MyUpload />
|
||||||
</ImgCrop>
|
</ImgCrop>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue