comments and youtube and videos on editor
parent
00dee1ba9e
commit
e6b9a6d0e8
|
@ -20,6 +20,7 @@
|
|||
"@types/node": "^16.18.106",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vidstack/react": "^1.12.9",
|
||||
"antd": "^5.20.3",
|
||||
"antd-img-crop": "^4.23.0",
|
||||
"buffer": "^6.0.3",
|
||||
|
@ -2744,6 +2745,31 @@
|
|||
"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": {
|
||||
"version": "0.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||
|
@ -5226,6 +5252,23 @@
|
|||
"integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
|
||||
"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": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz",
|
||||
|
@ -13878,6 +13921,15 @@
|
|||
"integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==",
|
||||
"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": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"@types/node": "^16.18.106",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@vidstack/react": "^1.12.9",
|
||||
"antd": "^5.20.3",
|
||||
"antd-img-crop": "^4.23.0",
|
||||
"buffer": "^6.0.3",
|
||||
|
|
|
@ -1,129 +1,118 @@
|
|||
import { Avatar, Flex } from "antd";
|
||||
import {
|
||||
isSideMenuCollapsed,
|
||||
setIsSideMenuCollapsed,
|
||||
} from "../SideMenu/sideMenuSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
EditOutlined,
|
||||
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";
|
||||
import { Avatar, Dropdown, Flex } from 'antd';
|
||||
import { isSideMenuCollapsed, setIsSideMenuCollapsed } from '../SideMenu/sideMenuSlice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { EditOutlined, EyeOutlined, LeftOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, SunOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { darkMode, setDarkMode } from '../../reducers/appSlice';
|
||||
import styles from './styles.module.css';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
|
||||
type HeaderBarProps = {
|
||||
theme?: "light" | "dark";
|
||||
onView?: () => void;
|
||||
onEdit?: () => void;
|
||||
backTo?: string;
|
||||
theme?: 'light' | 'dark';
|
||||
onView?: () => void;
|
||||
onEdit?: () => void;
|
||||
backTo?: string;
|
||||
};
|
||||
|
||||
export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
|
||||
const dispatch = useDispatch();
|
||||
export default function HeaderBar(props: HeaderBarProps = { theme: 'light' }) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
const isCollpased = useSelector(isSideMenuCollapsed);
|
||||
const isDarkMode = useSelector(darkMode);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style={{
|
||||
paddingTop: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
}}
|
||||
>
|
||||
<Flex align="center" gap={16}>
|
||||
<div
|
||||
className={
|
||||
props.theme === "light"
|
||||
? styles.containerLight
|
||||
: styles.containerDark
|
||||
}
|
||||
style={{ borderRadius: 28, padding: 4 }}
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Flex
|
||||
justify="space-between"
|
||||
align="center"
|
||||
style={{
|
||||
paddingTop: 12,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
}}
|
||||
>
|
||||
{isCollpased ? (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setIsSideMenuCollapsed(false))}
|
||||
>
|
||||
<MenuUnfoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setIsSideMenuCollapsed(true))}
|
||||
>
|
||||
<MenuFoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Flex align="center" gap={16}>
|
||||
<div className={props.theme === 'light' ? styles.containerLight : styles.containerDark} style={{ borderRadius: 28, padding: 4 }}>
|
||||
{isCollpased ? (
|
||||
<div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(false))}>
|
||||
<MenuUnfoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(true))}>
|
||||
<MenuFoldOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{props.backTo && (
|
||||
<Link to={props.backTo}>
|
||||
<Flex gap={4}>
|
||||
<LeftOutlined />
|
||||
<span>Back</span>
|
||||
{props.backTo && (
|
||||
<Link to={props.backTo}>
|
||||
<Flex gap={4}>
|
||||
<LeftOutlined />
|
||||
<span>Back</span>
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
</Link>
|
||||
)}
|
||||
</Flex>
|
||||
|
||||
<Flex
|
||||
align="center"
|
||||
className={
|
||||
props.theme === "light" ? styles.containerLight : styles.containerDark
|
||||
}
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
gap={8}
|
||||
>
|
||||
{props.onView && (
|
||||
<div className={styles.iconContainer} onClick={props.onView}>
|
||||
<EyeOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
<Flex
|
||||
align="center"
|
||||
className={props.theme === 'light' ? styles.containerLight : styles.containerDark}
|
||||
style={{
|
||||
borderRadius: 28,
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 4,
|
||||
paddingBottom: 4,
|
||||
}}
|
||||
gap={8}
|
||||
>
|
||||
{props.onView && (
|
||||
<div className={styles.iconContainer} onClick={props.onView}>
|
||||
<EyeOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{props.onEdit && (
|
||||
<div className={styles.iconContainer} onClick={props.onEdit}>
|
||||
<EditOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
{props.onEdit && (
|
||||
<div className={styles.iconContainer} onClick={props.onEdit}>
|
||||
<EditOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isDarkMode ? (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setDarkMode(false))}
|
||||
>
|
||||
<SunOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.iconContainer}
|
||||
onClick={() => dispatch(setDarkMode(true))}
|
||||
>
|
||||
<MoonOutlined className={styles.icon} />
|
||||
</div>
|
||||
)}
|
||||
<Avatar size="default" icon={<UserOutlined />} />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
{isDarkMode ? (
|
||||
<div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(false))}>
|
||||
<SunOutlined className={styles.icon} />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(true))}>
|
||||
<MoonOutlined className={styles.icon} />
|
||||
</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 />} />
|
||||
</Dropdown>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
|
||||
/* return (
|
||||
/* return (
|
||||
<Header
|
||||
style={{
|
||||
position: "sticky",
|
||||
|
|
|
@ -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,30 +1,26 @@
|
|||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { setupListeners } from "@reduxjs/toolkit/query";
|
||||
import { sideMenuSlice } from "../components/SideMenu/sideMenuSlice";
|
||||
import { lessonPageEditorSlice } from "../../features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||
import { appSlice } from "../reducers/appSlice";
|
||||
import { lessonsApi } from "core/services/lessons";
|
||||
import { teamApi } from "core/services/team";
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { setupListeners } from '@reduxjs/toolkit/query';
|
||||
import { sideMenuSlice } from '../components/SideMenu/sideMenuSlice';
|
||||
import { lessonPageEditorSlice } from '../../features/Lessons/LessonPageEditor/lessonPageEditorSlice';
|
||||
import { appSlice } from '../reducers/appSlice';
|
||||
import { lessonsApi } from 'core/services/lessons';
|
||||
import { organizationApi } from 'core/services/organization';
|
||||
|
||||
const makeStore = (/* preloadedState */) => {
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
app: appSlice.reducer,
|
||||
sideMenu: sideMenuSlice.reducer,
|
||||
lessonPageEditor: lessonPageEditorSlice.reducer,
|
||||
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
||||
[teamApi.reducerPath]: teamApi.reducer,
|
||||
},
|
||||
// preloadedState,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(
|
||||
lessonsApi.middleware,
|
||||
teamApi.middleware
|
||||
),
|
||||
});
|
||||
const store = configureStore({
|
||||
reducer: {
|
||||
app: appSlice.reducer,
|
||||
sideMenu: sideMenuSlice.reducer,
|
||||
lessonPageEditor: lessonPageEditorSlice.reducer,
|
||||
[lessonsApi.reducerPath]: lessonsApi.reducer,
|
||||
[organizationApi.reducerPath]: organizationApi.reducer,
|
||||
},
|
||||
// preloadedState,
|
||||
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(lessonsApi.middleware, organizationApi.middleware),
|
||||
});
|
||||
|
||||
setupListeners(store.dispatch);
|
||||
return store;
|
||||
setupListeners(store.dispatch);
|
||||
return store;
|
||||
};
|
||||
|
||||
export const store = makeStore();
|
||||
|
|
|
@ -1,34 +1,55 @@
|
|||
export interface Lesson {
|
||||
Id: string;
|
||||
State: number;
|
||||
Title: string;
|
||||
ThumbnailUrl: string;
|
||||
CreatorUserId: string;
|
||||
CreatedAt: string;
|
||||
Id: string;
|
||||
State: number;
|
||||
Title: string;
|
||||
ThumbnailUrl: string;
|
||||
CreatorUserId: string;
|
||||
CreatedAt: string;
|
||||
}
|
||||
|
||||
export enum LessonState {
|
||||
Published = 1,
|
||||
Draft = 2,
|
||||
Published = 1,
|
||||
Draft = 2,
|
||||
}
|
||||
|
||||
// used for the preview card on /lessions page and on the lesson editor
|
||||
export interface LessonSettings {
|
||||
Title: string;
|
||||
ThumbnailUrl: string;
|
||||
State?: LessonState;
|
||||
Title: string;
|
||||
ThumbnailUrl: string;
|
||||
State?: LessonState;
|
||||
}
|
||||
|
||||
// used on lesson page and on the lesson editor
|
||||
export interface LessonContent {
|
||||
Id: string;
|
||||
Page: number;
|
||||
Position: number;
|
||||
Type: number;
|
||||
Data: string;
|
||||
Id: string;
|
||||
Page: number;
|
||||
Position: number;
|
||||
Type: number;
|
||||
Data: string;
|
||||
}
|
||||
|
||||
export interface UpdateLessonPreviewThumbnail {
|
||||
lessonId: string;
|
||||
formData: FormData;
|
||||
lessonId: string;
|
||||
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,48 +1,44 @@
|
|||
import { Buffer } from "buffer";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Buffer } from 'buffer';
|
||||
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 = {
|
||||
API_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/v1`,
|
||||
WS_ADDRESS: `${wssProtocol}${window.location.hostname}/apiws`,
|
||||
STATIC_CONTENT_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/static`,
|
||||
ROUTE_PATHS: {
|
||||
LESSIONS: {
|
||||
ROOT: "/lessons",
|
||||
PAGE: "/lessons/:lessonId",
|
||||
PAGE_EDITOR: "/lessons/:lessonId/editor",
|
||||
API_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/v1`,
|
||||
WS_ADDRESS: `${wssProtocol}${window.location.hostname}/apiws`,
|
||||
STATIC_CONTENT_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/static`,
|
||||
ROUTE_PATHS: {
|
||||
LESSIONS: {
|
||||
ROOT: '/lessons',
|
||||
PAGE: '/lessons/:lessonId',
|
||||
PAGE_EDITOR: '/lessons/:lessonId/editor',
|
||||
},
|
||||
ORGANIZATION_TEAM: '/team',
|
||||
ORGANIZATION_TEAM_CREATE_USER: '/team/create-user',
|
||||
ORGANIZATION_ROLES: '/roles',
|
||||
ORGANIZATION_SETTINGS: '/organization',
|
||||
ACCOUNT_SETTINGS: '/account',
|
||||
WHATS_NEW: '/whats-new',
|
||||
SUGGEST_FEATURE: '/suggest-feature',
|
||||
CONTACT_SUPPORT: '/contact-support',
|
||||
},
|
||||
ORGANIZATION_TEAM: "/team",
|
||||
ORGANIZATION_TEAM_CREATE_USER: "/team/create-user",
|
||||
ORGANIZATION_ROLES: "/roles",
|
||||
ORGANIZATION_SETTINGS: "/organization",
|
||||
ACCOUNT_SETTINGS: "/account",
|
||||
WHATS_NEW: "/whats-new",
|
||||
SUGGEST_FEATURE: "/suggest-feature",
|
||||
CONTACT_SUPPORT: "/contact-support",
|
||||
},
|
||||
STYLES: {
|
||||
BLACK: "#000",
|
||||
},
|
||||
MAX_IMAGE_SIZE: 25 * 1024 * 1024, // 25MB
|
||||
ACCEPTED_IMAGE_FILE_TYPES: [
|
||||
"image/png",
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/webp",
|
||||
],
|
||||
STYLES: {
|
||||
BLACK: '#000',
|
||||
},
|
||||
MAX_IMAGE_SIZE: 25 * 1024 * 1024, // 25MB
|
||||
ACCEPTED_IMAGE_FILE_TYPES: ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'],
|
||||
ACCEPTED_VIDEO_FILE_TYPES: ['video/mp4', 'video/webm', 'video/mkv'],
|
||||
};
|
||||
|
||||
// used for sideMenu
|
||||
export const BreakpointLgWidth = 992;
|
||||
|
||||
export function GetUuid() {
|
||||
return uuidv4();
|
||||
return uuidv4();
|
||||
}
|
||||
|
||||
export function getImageUrl(imageName: string) {
|
||||
return `${Constants.STATIC_CONTENT_ADDRESS}/${imageName}`;
|
||||
return `${Constants.STATIC_CONTENT_ADDRESS}/${imageName}`;
|
||||
}
|
||||
|
||||
// needed for a user who uses multiple tabs in the browser
|
||||
|
@ -53,140 +49,140 @@ export function getImageUrl(imageName: string) {
|
|||
export const BrowserTabSession = GetUuid();
|
||||
|
||||
export function getUserSessionFromLocalStorage() {
|
||||
return localStorage.getItem("session");
|
||||
return localStorage.getItem('session');
|
||||
}
|
||||
|
||||
export function EncodeStringToBase64(value: string) {
|
||||
return Buffer.from(value).toString("base64");
|
||||
return Buffer.from(value).toString('base64');
|
||||
}
|
||||
|
||||
export function DecodedBase64ToString(value: string) {
|
||||
return Buffer.from(value, "base64").toString();
|
||||
return Buffer.from(value, 'base64').toString();
|
||||
}
|
||||
|
||||
export function handleLogout() {
|
||||
localStorage.removeItem("session");
|
||||
window.location.href = "/";
|
||||
localStorage.removeItem('session');
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
export const myFetchContentType = {
|
||||
JSON: 0,
|
||||
FORM_DATA: 1,
|
||||
JSON: 0,
|
||||
FORM_DATA: 1,
|
||||
};
|
||||
|
||||
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
|
||||
interface MyFetchOptions<TRequest = any, TResponse = any> {
|
||||
url?: string;
|
||||
method?: Method;
|
||||
body?: TRequest | null;
|
||||
headers?: Record<string, string>;
|
||||
contentType?: "JSON" | "FORM_DATA";
|
||||
fetchUrl?: string;
|
||||
ignoreUnauthorized?: boolean;
|
||||
notificationApi?: any; // Passen Sie dies je nach Bedarf an
|
||||
t?: any; // Passen Sie dies je nach Bedarf an
|
||||
url?: string;
|
||||
method?: Method;
|
||||
body?: TRequest | null;
|
||||
headers?: Record<string, string>;
|
||||
contentType?: 'JSON' | 'FORM_DATA';
|
||||
fetchUrl?: string;
|
||||
ignoreUnauthorized?: boolean;
|
||||
notificationApi?: any; // Passen Sie dies je nach Bedarf an
|
||||
t?: any; // Passen Sie dies je nach Bedarf an
|
||||
}
|
||||
|
||||
export function myFetch<TRequest = any, TResponse = any>({
|
||||
url = "",
|
||||
method = "GET",
|
||||
body = null,
|
||||
headers = {},
|
||||
contentType = "JSON",
|
||||
fetchUrl = Constants.API_ADDRESS,
|
||||
ignoreUnauthorized = false,
|
||||
notificationApi = null,
|
||||
t = null,
|
||||
url = '',
|
||||
method = 'GET',
|
||||
body = null,
|
||||
headers = {},
|
||||
contentType = 'JSON',
|
||||
fetchUrl = Constants.API_ADDRESS,
|
||||
ignoreUnauthorized = false,
|
||||
notificationApi = null,
|
||||
t = null,
|
||||
}: MyFetchOptions<TRequest, TResponse>): Promise<TResponse> {
|
||||
const getContentType = () => {
|
||||
if (contentType === "JSON") return "application/json";
|
||||
const getContentType = () => {
|
||||
if (contentType === 'JSON') return 'application/json';
|
||||
|
||||
return "multipart/form-data";
|
||||
};
|
||||
return 'multipart/form-data';
|
||||
};
|
||||
|
||||
const getBody = () => {
|
||||
if (!body) return null;
|
||||
const getBody = () => {
|
||||
if (!body) return null;
|
||||
|
||||
if (contentType === "JSON") return JSON.stringify(body);
|
||||
if (contentType === 'JSON') return JSON.stringify(body);
|
||||
|
||||
return body;
|
||||
};
|
||||
return body;
|
||||
};
|
||||
|
||||
// abort fetch if it takes to long
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
// abort fetch if it takes to long
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
setTimeout(() => controller.abort(), 30000); // 30 seconds
|
||||
setTimeout(() => controller.abort(), 30000); // 30 seconds
|
||||
|
||||
const requestOptions = {
|
||||
method: method,
|
||||
headers: {
|
||||
"X-Authorization": getUserSessionFromLocalStorage() || "",
|
||||
"Content-Type": getContentType(),
|
||||
...headers,
|
||||
},
|
||||
body: getBody(),
|
||||
signal: signal,
|
||||
};
|
||||
const requestOptions = {
|
||||
method: method,
|
||||
headers: {
|
||||
'X-Authorization': getUserSessionFromLocalStorage() || '',
|
||||
'Content-Type': getContentType(),
|
||||
...headers,
|
||||
},
|
||||
body: getBody(),
|
||||
signal: signal,
|
||||
};
|
||||
|
||||
if (fetchUrl === "") {
|
||||
fetchUrl = Constants.API_ADDRESS;
|
||||
}
|
||||
if (fetchUrl === '') {
|
||||
fetchUrl = Constants.API_ADDRESS;
|
||||
}
|
||||
|
||||
return fetch(`${fetchUrl}${url}`, requestOptions as RequestInit)
|
||||
.then(async (response) => {
|
||||
// if status is not in range 200-299
|
||||
if (!response.ok) {
|
||||
if (!ignoreUnauthorized && response.status === 401) {
|
||||
console.error("Unauthorized");
|
||||
// TODO: check here
|
||||
//setUserSessionToLocalStorage("");
|
||||
//window.location.href = "/";
|
||||
}
|
||||
return fetch(`${fetchUrl}${url}`, requestOptions as RequestInit)
|
||||
.then(async (response) => {
|
||||
// if status is not in range 200-299
|
||||
if (!response.ok) {
|
||||
if (!ignoreUnauthorized && response.status === 401) {
|
||||
console.error('Unauthorized');
|
||||
// TODO: check here
|
||||
//setUserSessionToLocalStorage("");
|
||||
//window.location.href = "/";
|
||||
}
|
||||
|
||||
throw response.status;
|
||||
}
|
||||
throw response.status;
|
||||
}
|
||||
|
||||
// check if response is json
|
||||
if (response.headers.get("content-type")?.includes("application/json")) {
|
||||
return response.json();
|
||||
}
|
||||
// check if response is json
|
||||
if (response.headers.get('content-type')?.includes('application/json')) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
return response.text();
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error("Error", error);
|
||||
return response.text();
|
||||
})
|
||||
.catch(async (error) => {
|
||||
console.error('Error', error);
|
||||
|
||||
// ignore errors here as they are handled in the components
|
||||
if (error === 400) throw error;
|
||||
// ignore errors here as they are handled in the components
|
||||
if (error === 400) throw error;
|
||||
|
||||
if (error === 401) {
|
||||
if (ignoreUnauthorized) {
|
||||
throw error;
|
||||
}
|
||||
if (error === 401) {
|
||||
if (ignoreUnauthorized) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
handleLogout();
|
||||
return;
|
||||
}
|
||||
handleLogout();
|
||||
return;
|
||||
}
|
||||
|
||||
// show error notification for all other errors
|
||||
// show error notification for all other errors
|
||||
|
||||
if (notificationApi !== null && t !== null) {
|
||||
if (error === 500) {
|
||||
/* notificationApi["error"]({
|
||||
if (notificationApi !== null && t !== null) {
|
||||
if (error === 500) {
|
||||
/* notificationApi["error"]({
|
||||
message: t("common.request.failed.title"),
|
||||
description: t("common.request.failed.description"),
|
||||
}); */
|
||||
} else {
|
||||
// other errors
|
||||
/* notificationApi["error"]({
|
||||
} else {
|
||||
// other errors
|
||||
/* notificationApi["error"]({
|
||||
message: t("common.request.failedInternetProblem.title"),
|
||||
description: t("common.request.failedInternetProblem.description"),
|
||||
}); */
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
});
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import {
|
|||
Avatar,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Divider,
|
||||
Flex,
|
||||
Form,
|
||||
|
@ -18,78 +19,31 @@ import ColorPicker from "antd/es/color-picker";
|
|||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
import Meta from "antd/es/card/Meta";
|
||||
|
||||
export function AccountSettingsAdmin() {
|
||||
return (
|
||||
<>
|
||||
<MyBanner
|
||||
title="Account Settings"
|
||||
subtitle="MANAGE"
|
||||
headerBar={<HeaderBar />}
|
||||
/>
|
||||
export default function AccountSettings({ isAdmin }: { isAdmin?: boolean }) {
|
||||
function AdminWrapper({ children }: { children: React.ReactNode }) {
|
||||
if (!isAdmin) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
<MyMiddleCard title="My Profile">
|
||||
<Flex vertical gap={16} >
|
||||
<Card
|
||||
styles={{
|
||||
body: {
|
||||
padding: 16,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Meta
|
||||
avatar={
|
||||
<Avatar
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
||||
size={56}
|
||||
/>
|
||||
}
|
||||
title="Jorg Kreith"
|
||||
description="Lead"
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
styles={{
|
||||
body: {
|
||||
padding: 16,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Meta title="Personal Information" />
|
||||
<Form layout="vertical" style={{ marginTop: 24 }}>
|
||||
<Flex gap={16}>
|
||||
<Flex flex={1}>
|
||||
<Form.Item label="First name" name="firstName" style={{width: "100%"}}>
|
||||
<Input defaultValue="Jorg" />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Flex flex={1}>
|
||||
<Form.Item label="Last name" name="lastName" style={{width: "100%"}}>
|
||||
<Input defaultValue="Kreth" />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Form.Item label="Email" name="email">
|
||||
<Input defaultValue="julian@xx.com" />
|
||||
</Form.Item>
|
||||
<Form.Item style={{ textAlign: 'right' }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
htmlType="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Flex>
|
||||
</MyMiddleCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Form layout="vertical" style={{ marginTop: 24 }}>
|
||||
{children}
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function TextItem({ value, name }: { value: string; name: string }) {
|
||||
if (!isAdmin) {
|
||||
return <>{value}</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item name={name} style={{ width: "100%" }} required>
|
||||
<Input defaultValue={value} />
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AccountSettings() {
|
||||
return (
|
||||
<>
|
||||
<MyBanner
|
||||
|
@ -118,7 +72,7 @@ export default function AccountSettings() {
|
|||
description="Lead"
|
||||
/>
|
||||
</Card>
|
||||
<Card
|
||||
{/*<Card
|
||||
styles={{
|
||||
body: {
|
||||
padding: 16,
|
||||
|
@ -144,6 +98,37 @@ export default function AccountSettings() {
|
|||
</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>
|
||||
</Flex>
|
||||
</MyMiddleCard>
|
||||
|
|
|
@ -20,7 +20,7 @@ interface SignInFetchResponse {
|
|||
}
|
||||
|
||||
export default function SignIn() {
|
||||
const [form] = useForm();
|
||||
const [form] = useForm<FieldType>();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
|
|
@ -1,63 +1,63 @@
|
|||
import { Button, Flex } from "antd";
|
||||
import { CheckOutlined } from "@ant-design/icons";
|
||||
import HeaderBar from "../../../core/components/Header";
|
||||
import { useLocation, useNavigate, useParams } from "react-router-dom";
|
||||
import { Constants } from "../../../core/utils/utils";
|
||||
import React from "react";
|
||||
import MySpin from "shared/components/MySpin";
|
||||
import MyErrorResult from "shared/components/MyResult";
|
||||
import MyEmpty from "shared/components/MyEmpty";
|
||||
import { useGetLessonContentsQuery } from "core/services/lessons";
|
||||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
import { Converter } from "../converter";
|
||||
import { Button, Flex } from 'antd';
|
||||
import { CheckOutlined } from '@ant-design/icons';
|
||||
import HeaderBar from '../../../core/components/Header';
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
import { Constants } from '../../../core/utils/utils';
|
||||
import React from 'react';
|
||||
import MySpin from 'shared/components/MySpin';
|
||||
import MyErrorResult from 'shared/components/MyResult';
|
||||
import MyEmpty from 'shared/components/MyEmpty';
|
||||
import { useGetLessonContentsQuery } from 'core/services/lessons';
|
||||
import MyMiddleCard from 'shared/components/MyMiddleCard';
|
||||
import { Converter } from '../converter';
|
||||
import Questions from '../Questions';
|
||||
|
||||
const LessonContents: React.FC = () => {
|
||||
const { lessonId } = useParams();
|
||||
const { lessonId } = useParams();
|
||||
|
||||
const { data, error, isLoading } = useGetLessonContentsQuery(
|
||||
lessonId as string,
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
const { data, error, isLoading } = useGetLessonContentsQuery(lessonId as string, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
if (isLoading) return <MySpin />;
|
||||
if (error) return <MyErrorResult />;
|
||||
if (isLoading) return <MySpin />;
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
if (!data || data.length === 0) return <MyEmpty />;
|
||||
if (!data || data.length === 0) return <MyEmpty />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{data.map((lessonContent) => (
|
||||
<div key={lessonContent.Id} style={{ paddingBottom: 6 }}>
|
||||
<Converter mode="view" lessonContent={lessonContent} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
{data.map((lessonContent) => (
|
||||
<div key={lessonContent.Id} style={{ paddingBottom: 6 }}>
|
||||
<Converter mode="view" lessonContent={lessonContent} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function LessonPage() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderBar
|
||||
theme="light"
|
||||
backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT}
|
||||
onEdit={() => navigate(`${location.pathname}/editor`)}
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
<HeaderBar theme="light" backTo={Constants.ROUTE_PATHS.LESSIONS.ROOT} onEdit={() => navigate(`${location.pathname}/editor`)} />
|
||||
|
||||
<MyMiddleCard>
|
||||
<LessonContents />
|
||||
<MyMiddleCard
|
||||
outOfCardChildren={
|
||||
<div style={{ marginTop: 24 }}>
|
||||
<Questions lessionID={'lessionID'} />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LessonContents />
|
||||
|
||||
<Flex justify="right">
|
||||
<Button type="primary" icon={<CheckOutlined />}>
|
||||
Finish lesson
|
||||
</Button>
|
||||
</Flex>
|
||||
</MyMiddleCard>
|
||||
</>
|
||||
);
|
||||
<Flex justify="right">
|
||||
<Button type="primary" icon={<CheckOutlined />}>
|
||||
Finish lesson
|
||||
</Button>
|
||||
</Flex>
|
||||
</MyMiddleCard>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,76 +1,76 @@
|
|||
import { DndContext, DragEndEvent, useDroppable } from "@dnd-kit/core";
|
||||
import {
|
||||
verticalListSortingStrategy,
|
||||
SortableContext,
|
||||
} from "@dnd-kit/sortable";
|
||||
import SortableEditorItem from "./SortableEditorItem";
|
||||
import { store } from "core/store/store";
|
||||
import { closestCenter, closestCorners, DndContext, DragEndEvent, DragOverlay, MeasuringStrategy, rectIntersection, useDroppable } from '@dnd-kit/core';
|
||||
import { verticalListSortingStrategy, SortableContext, rectSwappingStrategy, rectSortingStrategy } from '@dnd-kit/sortable';
|
||||
import SortableEditorItem from './SortableEditorItem';
|
||||
import { store } from 'core/store/store';
|
||||
|
||||
import {
|
||||
restrictToVerticalAxis,
|
||||
restrictToWindowEdges,
|
||||
} from "@dnd-kit/modifiers";
|
||||
import { currentLessonId, onDragHandler } from "./lessonPageEditorSlice";
|
||||
import { LessonContent } from "core/types/lesson";
|
||||
import { useUpdateLessonContentPositionMutation } from "core/services/lessons";
|
||||
import { useSelector } from "react-redux";
|
||||
import { restrictToVerticalAxis, restrictToWindowEdges, snapCenterToCursor } from '@dnd-kit/modifiers';
|
||||
import { currentLessonId, onDragHandler } from './lessonPageEditorSlice';
|
||||
import { LessonContent } from 'core/types/lesson';
|
||||
import { useUpdateLessonContentPositionMutation } from 'core/services/lessons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import React from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { HolderOutlined } from '@ant-design/icons';
|
||||
|
||||
const Droppable = ({ items }: { items: LessonContent[] }) => {
|
||||
const droppableID = "editorComponentArea";
|
||||
const { setNodeRef } = useDroppable({ id: droppableID });
|
||||
const currentLnId = useSelector(currentLessonId);
|
||||
const droppableID = 'editorComponentArea';
|
||||
const { setNodeRef } = useDroppable({ id: droppableID });
|
||||
const currentLnId = useSelector(currentLessonId);
|
||||
|
||||
const [reqUpdateLessonContentPosition] =
|
||||
useUpdateLessonContentPositionMutation();
|
||||
const [reqUpdateLessonContentPosition] = useUpdateLessonContentPositionMutation();
|
||||
|
||||
const itemIDs = items.map((item) => item.Id);
|
||||
const [isDragging, setIsDragging] = React.useState(false);
|
||||
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
console.log("drag end", event);
|
||||
const itemIDs = items.map((item) => item.Id);
|
||||
|
||||
if (!event.over) return;
|
||||
const handleDragEnd = (event: DragEndEvent) => {
|
||||
console.log('drag end', event);
|
||||
setIsDragging(false);
|
||||
|
||||
const activeId = event.active.id;
|
||||
const overId = event.over.id;
|
||||
if (!event.over) return;
|
||||
|
||||
if (activeId === overId) return;
|
||||
const activeId = event.active.id;
|
||||
const overId = event.over.id;
|
||||
|
||||
let oldIndex = itemIDs.findIndex((item) => item === activeId);
|
||||
let newIndex = itemIDs.findIndex((item) => item === overId);
|
||||
if (activeId === overId) return;
|
||||
|
||||
// store.dispatch(onDragHandler({ activeId, overId }));
|
||||
let oldIndex = itemIDs.findIndex((item) => item === activeId);
|
||||
let newIndex = itemIDs.findIndex((item) => item === overId);
|
||||
|
||||
store.dispatch(onDragHandler({ oldIndex, newIndex }));
|
||||
// store.dispatch(onDragHandler({ activeId, overId }));
|
||||
|
||||
try {
|
||||
reqUpdateLessonContentPosition({
|
||||
lessonId: currentLnId,
|
||||
contentId: activeId,
|
||||
newPosition: newIndex + 1,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
store.dispatch(onDragHandler({ oldIndex, newIndex }));
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
modifiers={[restrictToVerticalAxis, restrictToWindowEdges]}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
id={droppableID}
|
||||
items={itemIDs}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div ref={setNodeRef}>
|
||||
{items.map((item) => (
|
||||
<SortableEditorItem key={`${droppableID}_${item.Id}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
);
|
||||
try {
|
||||
reqUpdateLessonContentPosition({
|
||||
lessonId: currentLnId,
|
||||
contentId: activeId,
|
||||
newPosition: newIndex + 1,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
modifiers={[snapCenterToCursor]}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={() => {
|
||||
setIsDragging(true);
|
||||
}}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext id={droppableID} items={itemIDs} strategy={verticalListSortingStrategy}>
|
||||
<div ref={setNodeRef}>
|
||||
{items.map((item) => (
|
||||
<SortableEditorItem key={`${droppableID}_${item.Id}`} item={item} />
|
||||
))}
|
||||
</div>
|
||||
</SortableContext>
|
||||
<DragOverlay>{isDragging ? <HolderOutlined style={{ cursor: 'grabbing' }} /> : null}</DragOverlay>
|
||||
</DndContext>
|
||||
);
|
||||
};
|
||||
/*
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
|
|
|
@ -1,110 +1,107 @@
|
|||
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import {
|
||||
HolderOutlined,
|
||||
DeleteOutlined,
|
||||
CameraOutlined,
|
||||
FolderOpenOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { Flex } from "antd";
|
||||
import {
|
||||
currentLessonId,
|
||||
deleteLessonContent,
|
||||
updateLessonContent,
|
||||
} from "./lessonPageEditorSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { getComponentByType } from "../components";
|
||||
import { LessonContent } from "core/types/lesson";
|
||||
import "./styles.module.css";
|
||||
import { Converter } from "../converter";
|
||||
import { useDeleteLessonContentMutation } from "core/services/lessons";
|
||||
import { defaultAnimateLayoutChanges, useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { HolderOutlined, DeleteOutlined, CameraOutlined, FolderOpenOutlined } from '@ant-design/icons';
|
||||
import { Flex } from 'antd';
|
||||
import { currentLessonId, deleteLessonContent, updateLessonContent } from './lessonPageEditorSlice';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { getComponentByType } from '../components';
|
||||
import { LessonContent } from 'core/types/lesson';
|
||||
import './styles.module.css';
|
||||
import { Converter } from '../converter';
|
||||
import { useDeleteLessonContentMutation } from 'core/services/lessons';
|
||||
|
||||
const animateLayoutChanges = (args: any) =>
|
||||
args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true;
|
||||
const animateLayoutChanges = (args: any) => (args.isSorting || args.wasDragging ? defaultAnimateLayoutChanges(args) : true);
|
||||
|
||||
const SortableEditorItem = (props: { item: LessonContent }) => {
|
||||
const lnContent = props.item;
|
||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||
useSortable({ id: lnContent.Id, animateLayoutChanges });
|
||||
const lnContent = props.item;
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: lnContent.Id });
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const component = getComponentByType(lnContent.Type);
|
||||
const component = getComponentByType(lnContent.Type);
|
||||
|
||||
const [reqDeleteLessonContent] = useDeleteLessonContentMutation();
|
||||
const currentLnId = useSelector(currentLessonId);
|
||||
const [reqDeleteLessonContent] = useDeleteLessonContentMutation();
|
||||
const currentLnId = useSelector(currentLessonId);
|
||||
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
if (!component) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
}}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
>
|
||||
<Flex key={lnContent.Id}>
|
||||
<HolderOutlined
|
||||
style={{
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
touchAction: "none",
|
||||
cursor: "move",
|
||||
}}
|
||||
{...listeners}
|
||||
/>
|
||||
<Converter
|
||||
mode="edititable"
|
||||
lessonContent={lnContent}
|
||||
onEdit={(data) => {
|
||||
console.log("edit", lnContent.Id, data);
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
transform: CSS.Translate.toString(transform),
|
||||
transition,
|
||||
}}
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
>
|
||||
<Flex key={lnContent.Id}>
|
||||
<Flex>
|
||||
<HolderOutlined
|
||||
style={{
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
touchAction: 'none',
|
||||
cursor: 'grab',
|
||||
opacity: isDragging ? 0 : 1,
|
||||
}}
|
||||
{...listeners}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex style={{ overflow: 'hidden', width: '100%', transition: '', boxShadow: isDragging ? 'rgba(0, 0, 0, 0.35) 0px 5px 15px;' : '' }}>
|
||||
<Converter
|
||||
mode="edititable"
|
||||
lessonContent={lnContent}
|
||||
onEdit={(data) => {
|
||||
console.log('edit', lnContent.Id, data);
|
||||
|
||||
dispatch(
|
||||
updateLessonContent({
|
||||
id: lnContent.Id,
|
||||
data: data,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
dispatch(
|
||||
updateLessonContent({
|
||||
id: lnContent.Id,
|
||||
data: data,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Flex vertical justify="center">
|
||||
{component.uploadImage ? (
|
||||
<div className="EditorActionIcon">
|
||||
<CameraOutlined />
|
||||
</div>
|
||||
) : null}
|
||||
{component.uploadFileTypes ? (
|
||||
<div className="EditorActionIcon">
|
||||
<FolderOpenOutlined className="EditorActionIcon" />{" "}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="EditorActionIcon">
|
||||
<DeleteOutlined
|
||||
className="EditorActionIcon"
|
||||
onClick={() => {
|
||||
console.log("delete", lnContent.Id);
|
||||
dispatch(deleteLessonContent(lnContent.Id));
|
||||
<Flex vertical justify="center" style={{ paddingLeft: 12 }}>
|
||||
<div className="EditorActionIcon">
|
||||
<DeleteOutlined
|
||||
className="EditorActionIcon"
|
||||
onClick={() => {
|
||||
console.log('delete', lnContent.Id);
|
||||
dispatch(deleteLessonContent(lnContent.Id));
|
||||
|
||||
try {
|
||||
reqDeleteLessonContent({
|
||||
lessonId: currentLnId,
|
||||
contentId: lnContent.Id,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
try {
|
||||
reqDeleteLessonContent({
|
||||
lessonId: currentLnId,
|
||||
contentId: lnContent.Id,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
{component.uploadImage ? (
|
||||
<div className="EditorActionIcon">
|
||||
<CameraOutlined />
|
||||
</div>
|
||||
) : null}
|
||||
{component.uploadFileTypes ? (
|
||||
<div className="EditorActionIcon">
|
||||
<FolderOpenOutlined className="EditorActionIcon" />{' '}
|
||||
</div>
|
||||
) : null}
|
||||
*/
|
||||
|
||||
export default SortableEditorItem;
|
||||
|
|
|
@ -1,156 +1,120 @@
|
|||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
lessonContents,
|
||||
lessonThumbnail,
|
||||
setCurrentLessonId,
|
||||
setEditorActive,
|
||||
setLessonContents,
|
||||
setLessonState,
|
||||
} from "./lessonPageEditorSlice";
|
||||
import { useEffect } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { Card, Flex } from "antd";
|
||||
import { Constants } from "core/utils/utils";
|
||||
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";
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { lessonContents, lessonThumbnail, setCurrentLessonId, setEditorActive, setLessonContents, setLessonState } from './lessonPageEditorSlice';
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { Card, Flex } from 'antd';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
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 dispatch = useDispatch();
|
||||
const { lessonId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const { lessonId } = useParams();
|
||||
|
||||
const { data, error, isLoading, refetch } = useGetLessonSettingsQuery(
|
||||
lessonId as string,
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
const { data, error, isLoading, refetch } = useGetLessonSettingsQuery(lessonId as string, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
const [updateLessonPreviewTitle] = useUpdateLessonPreviewTitleMutation();
|
||||
const [updateLessonPreviewTitle] = useUpdateLessonPreviewTitleMutation();
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.State) dispatch(setLessonState(data.State));
|
||||
}, [data]);
|
||||
useEffect(() => {
|
||||
if (data?.State) dispatch(setLessonState(data.State));
|
||||
}, [data]);
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
return (
|
||||
<LessonPreviewCard
|
||||
mode="editable"
|
||||
lessonId={lessonId as string}
|
||||
loading={isLoading}
|
||||
lessonSettings={{
|
||||
Title: data?.Title || "",
|
||||
ThumbnailUrl: data?.ThumbnailUrl || "",
|
||||
}}
|
||||
onEditTitle={async (newTitle) => {
|
||||
try {
|
||||
const res = await updateLessonPreviewTitle({
|
||||
lessonId: lessonId as string,
|
||||
newTitle: newTitle,
|
||||
}).unwrap();
|
||||
return (
|
||||
<LessonPreviewCard
|
||||
mode="editable"
|
||||
lessonId={lessonId as string}
|
||||
loading={isLoading}
|
||||
lessonSettings={{
|
||||
Title: data?.Title || '',
|
||||
ThumbnailUrl: data?.ThumbnailUrl || '',
|
||||
}}
|
||||
onEditTitle={async (newTitle) => {
|
||||
try {
|
||||
const res = await updateLessonPreviewTitle({
|
||||
lessonId: lessonId as string,
|
||||
newTitle: newTitle,
|
||||
}).unwrap();
|
||||
|
||||
if (res) {
|
||||
refetch();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}}
|
||||
onThumbnailChanged={refetch}
|
||||
/>
|
||||
);
|
||||
if (res) {
|
||||
refetch();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}}
|
||||
onThumbnailChanged={refetch}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const LessonContentComponents: React.FC = () => {
|
||||
const { lessonId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
const { lessonId } = useParams();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const { data, error, isLoading } = useGetLessonContentsQuery(
|
||||
lessonId as string,
|
||||
{
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
const { data, error, isLoading } = useGetLessonContentsQuery(lessonId as string, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
const lnContents = useSelector(lessonContents);
|
||||
const lnContents = useSelector(lessonContents);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
||||
dispatch(setLessonContents(data));
|
||||
}, [data]);
|
||||
dispatch(setLessonContents(data));
|
||||
}, [data]);
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
return (
|
||||
<Card loading={isLoading}>
|
||||
<Flex vertical gap={16}>
|
||||
{!lnContents || lnContents.length == 0 ? (
|
||||
<MyEmpty />
|
||||
) : (
|
||||
<Droppable items={lnContents} />
|
||||
)}
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
return (
|
||||
<Card loading={isLoading}>
|
||||
<Flex vertical gap={16}>
|
||||
{!lnContents || lnContents.length == 0 ? <MyEmpty /> : <Droppable items={lnContents} />}
|
||||
</Flex>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default function LessonPageEditor() {
|
||||
const { lessonId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { lessonId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const lnContents = useSelector(lessonContents);
|
||||
const lnThumbnail = useSelector(lessonThumbnail);
|
||||
const dispatch = useDispatch();
|
||||
const lnContents = useSelector(lessonContents);
|
||||
const lnThumbnail = useSelector(lessonThumbnail);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setEditorActive(true));
|
||||
dispatch(setCurrentLessonId(lessonId as string));
|
||||
useEffect(() => {
|
||||
dispatch(setEditorActive(true));
|
||||
dispatch(setCurrentLessonId(lessonId as string));
|
||||
|
||||
return () => {
|
||||
dispatch(setEditorActive(false));
|
||||
};
|
||||
}, []);
|
||||
return () => {
|
||||
dispatch(setEditorActive(false));
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderBar
|
||||
theme="light"
|
||||
backTo={Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(
|
||||
":lessonId",
|
||||
lessonId as string
|
||||
)}
|
||||
onView={() =>
|
||||
navigate(
|
||||
Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(
|
||||
":lessonId",
|
||||
lessonId as string
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
<HeaderBar
|
||||
theme="light"
|
||||
backTo={Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(':lessonId', lessonId as string)}
|
||||
onView={() => navigate(Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(':lessonId', lessonId as string))}
|
||||
/>
|
||||
|
||||
<Flex justify="center" style={{ paddingTop: 24 }}>
|
||||
<Flex
|
||||
justify="center"
|
||||
vertical
|
||||
gap={16}
|
||||
className={styles.cardContainer}
|
||||
>
|
||||
<PreviewCard />
|
||||
<Flex justify="center" style={{ paddingTop: 24 }}>
|
||||
<Flex justify="center" vertical gap={16} className={styles.cardContainer}>
|
||||
<PreviewCard />
|
||||
|
||||
<LessonContentComponents />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
<LessonContentComponents />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
*/
|
|
@ -1,64 +1,65 @@
|
|||
// Desc: This file contains the list of components that are used in the Lessons
|
||||
|
||||
type ComponentGroup = {
|
||||
category: string;
|
||||
components: Component[];
|
||||
category: string;
|
||||
components: Component[];
|
||||
};
|
||||
|
||||
export type Component = {
|
||||
type: number;
|
||||
name: string;
|
||||
thumbnail?: string;
|
||||
invertThumbnailAtDarkmode?: boolean;
|
||||
uploadFileTypes?: string[];
|
||||
uploadImage?: boolean;
|
||||
defaultData?: string;
|
||||
type: number;
|
||||
name: string;
|
||||
thumbnail?: string;
|
||||
invertThumbnailAtDarkmode?: boolean;
|
||||
uploadFileTypes?: string[];
|
||||
uploadImage?: boolean;
|
||||
defaultData?: string;
|
||||
};
|
||||
|
||||
const componentsGroups: ComponentGroup[] = [
|
||||
{
|
||||
category: "Common",
|
||||
components: [
|
||||
{
|
||||
type: 0,
|
||||
name: "Header",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_header.svg",
|
||||
invertThumbnailAtDarkmode: true,
|
||||
defaultData: "Header",
|
||||
},
|
||||
{
|
||||
type: 1,
|
||||
name: "Text",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_text.svg",
|
||||
invertThumbnailAtDarkmode: true,
|
||||
defaultData: "Text",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Media",
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
name: "Image",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_image.png",
|
||||
uploadImage: true,
|
||||
uploadFileTypes: ["image/*"],
|
||||
},
|
||||
{
|
||||
type: 3,
|
||||
name: "YouTube",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
||||
invertThumbnailAtDarkmode: true,
|
||||
},
|
||||
{
|
||||
type: 4,
|
||||
name: "Video",
|
||||
thumbnail: "/editor/thumbnails/component_thumbnail_youtube.png",
|
||||
invertThumbnailAtDarkmode: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Common',
|
||||
components: [
|
||||
{
|
||||
type: 0,
|
||||
name: 'Header',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_header.svg',
|
||||
invertThumbnailAtDarkmode: true,
|
||||
defaultData: 'Header',
|
||||
},
|
||||
{
|
||||
type: 1,
|
||||
name: 'Text',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_text.svg',
|
||||
invertThumbnailAtDarkmode: true,
|
||||
defaultData: 'Text',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
category: 'Media',
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
name: 'Image',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_image.png',
|
||||
uploadImage: true,
|
||||
uploadFileTypes: ['image/*'],
|
||||
},
|
||||
{
|
||||
type: 3,
|
||||
name: 'YouTube',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_youtube.png',
|
||||
invertThumbnailAtDarkmode: true,
|
||||
},
|
||||
{
|
||||
type: 4,
|
||||
name: 'Video',
|
||||
thumbnail: '/editor/thumbnails/component_thumbnail_youtube.png',
|
||||
invertThumbnailAtDarkmode: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
/*
|
||||
{
|
||||
category: "HTML",
|
||||
components: [
|
||||
|
@ -79,34 +80,34 @@ const componentsGroups: ComponentGroup[] = [
|
|||
thumbnail: "/editor/thumbnails/component_thumbnail_banner.png",
|
||||
},
|
||||
],
|
||||
},
|
||||
}, */
|
||||
];
|
||||
|
||||
const componentsMap: { [key: string]: Component } = (() => {
|
||||
const map: { [key: string]: Component } = {};
|
||||
const map: { [key: string]: Component } = {};
|
||||
|
||||
for (const group of componentsGroups) {
|
||||
for (const component of group.components) {
|
||||
map[component.name] = component;
|
||||
for (const group of componentsGroups) {
|
||||
for (const component of group.components) {
|
||||
map[component.name] = component;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
return map;
|
||||
})();
|
||||
|
||||
export function getTypeByName(name: string): number {
|
||||
const component = componentsMap[name];
|
||||
const component = componentsMap[name];
|
||||
|
||||
return component ? component.type : -1;
|
||||
return component ? component.type : -1;
|
||||
}
|
||||
|
||||
export function getComponentByType(type: number): Component | null {
|
||||
for (const component of Object.values(componentsMap)) {
|
||||
if (component.type === type) {
|
||||
return component;
|
||||
for (const component of Object.values(componentsMap)) {
|
||||
if (component.type === type) {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
export { componentsGroups };
|
||||
|
|
|
@ -1,183 +1,341 @@
|
|||
import { LessonContent } from "core/types/lesson";
|
||||
import { getTypeByName } from "./components";
|
||||
import { Button, Input, Typography } from "antd";
|
||||
import { useUpdateLessonContentMutation } from "core/services/lessons";
|
||||
import { useSelector } from "react-redux";
|
||||
import { currentLessonId } from "./LessonPageEditor/lessonPageEditorSlice";
|
||||
import { useRef } from "react";
|
||||
import MyUpload from "shared/components/MyUpload";
|
||||
import { LessonContent } from 'core/types/lesson';
|
||||
import { getTypeByName } from './components';
|
||||
import { Button, Input, Typography, Flex } from 'antd';
|
||||
import { useUpdateLessonContentMutation } from 'core/services/lessons';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { currentLessonId } from './LessonPageEditor/lessonPageEditorSlice';
|
||||
import { useRef, useEffect } from 'react';
|
||||
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({
|
||||
mode,
|
||||
lessonContent,
|
||||
onEdit,
|
||||
}: {
|
||||
mode: "view" | "edititable";
|
||||
lessonContent: LessonContent;
|
||||
onEdit?: (newData: string) => void;
|
||||
}) {
|
||||
const lessonId = useSelector(currentLessonId);
|
||||
const extractVideoId = (url: string) => {
|
||||
// regex to extract video id from youtube url
|
||||
const regex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/(?:watch\?v=|embed\/|v\/|.+\?v=|.+\/|.+v=)?([^"&?\/\s]{11})/;
|
||||
const match = url.match(regex);
|
||||
return match ? match[1] : url;
|
||||
};
|
||||
|
||||
const [reqUpdateLessonContent] = useUpdateLessonContentMutation();
|
||||
export function Converter({ mode, lessonContent, onEdit }: { mode: 'view' | 'edititable'; lessonContent: LessonContent; onEdit?: (newData: string) => void }) {
|
||||
const lessonId = useSelector(currentLessonId);
|
||||
|
||||
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
||||
const [reqUpdateLessonContent] = useUpdateLessonContentMutation();
|
||||
|
||||
switch (lessonContent.Type) {
|
||||
case getTypeByName("Header"):
|
||||
if (mode === "view") {
|
||||
return (
|
||||
<div
|
||||
style={{ fontWeight: "bold", fontSize: 24, wordBreak: "break-all" }}
|
||||
>
|
||||
{lessonContent.Data}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
||||
|
||||
return (
|
||||
<Typography.Title
|
||||
editable={{
|
||||
triggerType: "text" as any,
|
||||
onChange: (event) => {
|
||||
onEdit?.(event);
|
||||
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: event,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
}}
|
||||
level={1}
|
||||
style={{
|
||||
margin: 0,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
{lessonContent.Data}
|
||||
</Typography.Title>
|
||||
);
|
||||
case getTypeByName("Text"):
|
||||
if (mode === "view") {
|
||||
return (
|
||||
<div style={{ fontSize: 16, wordBreak: "break-all" }}>
|
||||
{lessonContent.Data}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input.TextArea
|
||||
variant="borderless"
|
||||
placeholder="Input text here"
|
||||
style={{ width: "100%" }}
|
||||
value={lessonContent.Data}
|
||||
onChange={(event) => {
|
||||
console.log("edit");
|
||||
|
||||
onEdit?.(event.target.value);
|
||||
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
switch (lessonContent.Type) {
|
||||
case getTypeByName('Header'):
|
||||
if (mode === 'view') {
|
||||
return <div style={{ fontWeight: 'bold', fontSize: 24, wordBreak: 'break-all' }}>{lessonContent.Data}</div>;
|
||||
}
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: event.target.value,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case getTypeByName("Image"):
|
||||
console.log("image", lessonContent.Data);
|
||||
return (
|
||||
<Typography.Title
|
||||
editable={{
|
||||
triggerType: 'text' as any,
|
||||
onChange: (event) => {
|
||||
onEdit?.(event);
|
||||
|
||||
if (mode === "view" && lessonContent.Data === "") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: 120,
|
||||
width: "100%",
|
||||
backgroundColor: "#EBEBEB",
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
No image provided
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: event,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
},
|
||||
}}
|
||||
level={1}
|
||||
style={{
|
||||
margin: 0,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{lessonContent.Data}
|
||||
</Typography.Title>
|
||||
);
|
||||
case getTypeByName('Text'):
|
||||
if (mode === 'view') {
|
||||
const formattedText = lessonContent.Data.split('\n').map((line, index) => <li key={index}>{line || '\u00A0'}</li>);
|
||||
|
||||
if (lessonContent.Data === "") {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
height: 120,
|
||||
width: "100%",
|
||||
backgroundColor: "#EBEBEB",
|
||||
margin: "12px 12px 12px 0",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: "translate(-50%, -50%)",
|
||||
}}
|
||||
>
|
||||
<span>Choose image from</span>
|
||||
return <ul style={{ fontSize: 16, wordBreak: 'break-all', padding: 0, margin: 0, listStyleType: 'none' }}>{formattedText}</ul>;
|
||||
}
|
||||
|
||||
<MyUpload>
|
||||
<Button type="link">Gallery</Button>
|
||||
</MyUpload>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Input.TextArea
|
||||
autoSize
|
||||
variant="borderless"
|
||||
placeholder="Input text here"
|
||||
style={{ width: '100%', padding: 0, paddingTop: 4 }}
|
||||
value={lessonContent.Data}
|
||||
onChange={(event) => {
|
||||
console.log('edit');
|
||||
|
||||
return (
|
||||
<>
|
||||
<img src={lessonContent.Data} alt="img" style={{ width: "100%" }} />
|
||||
</>
|
||||
);
|
||||
case getTypeByName("YouTube"):
|
||||
return (
|
||||
<div style={{ fontWeight: "700", fontSize: 20, width: "100%" }}>
|
||||
{lessonContent.Data}
|
||||
</div>
|
||||
);
|
||||
case getTypeByName("Video"):
|
||||
return <div style={{ fontSize: 14, width: "100%" }}>Not implemented</div>;
|
||||
case getTypeByName("Iframe"):
|
||||
return <div style={{ fontSize: 14, width: "100%" }}>Not implemented</div>;
|
||||
case getTypeByName("Banner"):
|
||||
return <div style={{ fontSize: 14, width: "100%" }}>Not implemented</div>;
|
||||
onEdit?.(event.target.value);
|
||||
|
||||
default:
|
||||
return <div>Unknown type</div>;
|
||||
}
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
try {
|
||||
reqUpdateLessonContent({
|
||||
lessonId: lessonId,
|
||||
contentId: lessonContent.Id,
|
||||
data: event.target.value,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}, 1000);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
case getTypeByName('Image'):
|
||||
console.log('image', lessonContent.Data);
|
||||
|
||||
if (mode === 'view' && lessonContent.Data === '') {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: '#EBEBEB',
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
No image provided
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: 120,
|
||||
width: '100%',
|
||||
backgroundColor: '#EBEBEB',
|
||||
margin: '12px 12px 12px 0',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}
|
||||
>
|
||||
<span>Choose image from</span>
|
||||
|
||||
<GalleryUpload />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex vertical 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'):
|
||||
const videoId = extractVideoId(lessonContent.Data);
|
||||
|
||||
console.log('videoId', videoId);
|
||||
|
||||
return (
|
||||
<Flex vertical style={{ width: '100%', paddingBottom: 12 }}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
const VideoUpload = () => {
|
||||
return (
|
||||
<MyUpload
|
||||
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:
|
||||
return <div>Unknown type</div>;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,37 +6,70 @@ import { SaveOutlined } from "@ant-design/icons";
|
|||
import MyUpload from "shared/components/MyUpload";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import ColorPicker from "antd/es/color-picker";
|
||||
import { 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() {
|
||||
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 (
|
||||
<>
|
||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
|
||||
<MyContainer>
|
||||
<Flex vertical gap={16} style={{ paddingBottom: 16 }}>
|
||||
<Card
|
||||
styles={{
|
||||
body: {
|
||||
padding: 16,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Flex vertical gap={2}>
|
||||
<Form>
|
||||
<Flex justify="space-between" align="center">
|
||||
<Typography.Title level={5} style={{ margin: 0 }}>
|
||||
Branding
|
||||
</Typography.Title>
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
/>
|
||||
</Flex>
|
||||
<Divider style={{ margin: 0, padding: 0 }} />
|
||||
|
||||
<Form form={form} onFinish={handleSave}>
|
||||
<Card
|
||||
loading={isLoading}
|
||||
styles={{
|
||||
body: {
|
||||
padding: 16,
|
||||
},
|
||||
}}
|
||||
title="Branding"
|
||||
extra={
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex vertical gap={2}>
|
||||
<Flex gap={32}>
|
||||
<Flex vertical>
|
||||
<Typography.Text style={{ fontSize: 16 }}>
|
||||
|
@ -47,6 +80,7 @@ export default function Settings() {
|
|||
defaultValue="#1677ff"
|
||||
size="small"
|
||||
showText
|
||||
format="hex"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
|
@ -63,7 +97,11 @@ export default function Settings() {
|
|||
Subdomain
|
||||
</Typography.Text>
|
||||
<Form.Item name="subdomain">
|
||||
<Input addonBefore="https://" addonAfter=". jannex . de" defaultValue="mysite" />
|
||||
<Input
|
||||
addonBefore="https://"
|
||||
addonAfter=". jannex.de"
|
||||
defaultValue="mysite"
|
||||
/>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
<Flex vertical>
|
||||
|
@ -128,9 +166,9 @@ export default function Settings() {
|
|||
/>
|
||||
</MyUpload>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Flex>
|
||||
</Card>
|
||||
</Form>
|
||||
</Flex>
|
||||
</MyContainer>
|
||||
</>
|
||||
|
|
|
@ -1,102 +1,100 @@
|
|||
import MyTable from "shared/components/MyTable";
|
||||
import HeaderBar from "../../core/components/Header";
|
||||
import MyBanner from "../../shared/components/MyBanner";
|
||||
import { MyContainer } from "../../shared/components/MyContainer";
|
||||
import { Button, Flex } from "antd";
|
||||
import { UserAddOutlined } from "@ant-design/icons";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import { useGetTeamQuery } from "core/services/team";
|
||||
import MyErrorResult from "shared/components/MyResult";
|
||||
import MyTable from 'shared/components/MyTable';
|
||||
import HeaderBar from '../../core/components/Header';
|
||||
import MyBanner from '../../shared/components/MyBanner';
|
||||
import { MyContainer } from '../../shared/components/MyContainer';
|
||||
import { Button, Flex } from 'antd';
|
||||
import { UserAddOutlined } from '@ant-design/icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import { useGetTeamQuery } from 'core/services/organization';
|
||||
import MyErrorResult from 'shared/components/MyResult';
|
||||
|
||||
const TeamList: React.FC = () => {
|
||||
const { data, error, isLoading } = useGetTeamQuery(undefined, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
const getTableContent = () => {
|
||||
let items = [
|
||||
{
|
||||
title: "First name",
|
||||
dataIndex: "firstName",
|
||||
key: "firstName",
|
||||
},
|
||||
{
|
||||
title: "Last name",
|
||||
dataIndex: "lastName",
|
||||
key: "lastName",
|
||||
},
|
||||
{
|
||||
title: "Email",
|
||||
dataIndex: "email",
|
||||
key: "email",
|
||||
},
|
||||
{
|
||||
title: "Role",
|
||||
dataIndex: "role",
|
||||
key: "role",
|
||||
},
|
||||
{
|
||||
title: "Status",
|
||||
dataIndex: "status",
|
||||
key: "status",
|
||||
},
|
||||
{
|
||||
title: "Actions",
|
||||
dataIndex: "actions",
|
||||
key: "actions",
|
||||
},
|
||||
];
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const getTableItems = () => {
|
||||
let items = [] as any[];
|
||||
|
||||
if (!data) return items;
|
||||
|
||||
data.forEach((item) => {
|
||||
items.push({
|
||||
key: item.Id,
|
||||
firstName: item.FirstName,
|
||||
lastName: item.LastName,
|
||||
email: item.Email,
|
||||
role: item.Role,
|
||||
status: item.Status,
|
||||
});
|
||||
const { data, error, isLoading } = useGetTeamQuery(undefined, {
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
const getTableContent = () => {
|
||||
let items = [
|
||||
{
|
||||
title: 'First name',
|
||||
dataIndex: 'firstName',
|
||||
key: 'firstName',
|
||||
},
|
||||
{
|
||||
title: 'Last name',
|
||||
dataIndex: 'lastName',
|
||||
key: 'lastName',
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
dataIndex: 'email',
|
||||
key: 'email',
|
||||
},
|
||||
{
|
||||
title: 'Role',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
dataIndex: 'actions',
|
||||
key: 'actions',
|
||||
},
|
||||
];
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
return items;
|
||||
};
|
||||
|
||||
return (
|
||||
<MyTable loading={isLoading} columns={getTableContent()} dataSource={getTableItems()} pagination={false} />
|
||||
);
|
||||
const getTableItems = () => {
|
||||
let items = [] as any[];
|
||||
|
||||
if (!data) return items;
|
||||
|
||||
data.forEach((item) => {
|
||||
items.push({
|
||||
key: item.Id,
|
||||
firstName: item.FirstName,
|
||||
lastName: item.LastName,
|
||||
email: item.Email,
|
||||
role: item.Role,
|
||||
status: item.Status,
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
return <MyTable loading={isLoading} columns={getTableContent()} dataSource={getTableItems()} pagination={false} />;
|
||||
};
|
||||
|
||||
export default function Team() {
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Team" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Team" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
|
||||
<MyContainer
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<Flex justify="end">
|
||||
<Link to={Constants.ROUTE_PATHS.ORGANIZATION_TEAM_CREATE_USER}>
|
||||
<Button icon={<UserAddOutlined />}>Invite new member</Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
<MyContainer
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 16,
|
||||
}}
|
||||
>
|
||||
<Flex justify="end">
|
||||
<Link to={Constants.ROUTE_PATHS.ORGANIZATION_TEAM_CREATE_USER}>
|
||||
<Button icon={<UserAddOutlined />}>Invite new member</Button>
|
||||
</Link>
|
||||
</Flex>
|
||||
|
||||
<TeamList />
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
<TeamList />
|
||||
</MyContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,26 +1,27 @@
|
|||
import { Card, CardProps, Flex } from "antd";
|
||||
import { MyContainer } from "../MyContainer";
|
||||
import { Card, CardProps, Flex } from 'antd';
|
||||
import { MyContainer } from '../MyContainer';
|
||||
|
||||
interface MyMiddleCardProps extends CardProps {}
|
||||
|
||||
const MyMiddleCard: React.FC<MyMiddleCardProps> = ({ children,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<MyContainer>
|
||||
<Flex justify="center">
|
||||
<Card
|
||||
style={{
|
||||
width: 800,
|
||||
maxWidth: 800,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
</Flex>
|
||||
</MyContainer>
|
||||
);
|
||||
interface MyMiddleCardProps extends CardProps {
|
||||
outOfCardChildren?: React.ReactNode;
|
||||
}
|
||||
|
||||
export default MyMiddleCard;
|
||||
const MyMiddleCard: React.FC<MyMiddleCardProps> = ({ children, outOfCardChildren, ...props }) => {
|
||||
return (
|
||||
<MyContainer>
|
||||
<Flex justify="center">
|
||||
<Card
|
||||
style={{
|
||||
width: 800,
|
||||
maxWidth: 800,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
</Flex>
|
||||
{outOfCardChildren}
|
||||
</MyContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyMiddleCard;
|
||||
|
|
|
@ -1,58 +1,67 @@
|
|||
import ImgCrop, { ImgCropProps } from "antd-img-crop";
|
||||
import Upload from "antd/es/upload/Upload";
|
||||
import { getApiHeader } from "core/helper/api";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import ImgCrop, { ImgCropProps } from 'antd-img-crop';
|
||||
import Upload from 'antd/es/upload/Upload';
|
||||
import { getApiHeader } from 'core/helper/api';
|
||||
import { Constants } from 'core/utils/utils';
|
||||
import { Fragment } from 'react';
|
||||
|
||||
export default function MyUpload({
|
||||
children,
|
||||
imgCropProps,
|
||||
accept = Constants.ACCEPTED_IMAGE_FILE_TYPES.join(","),
|
||||
maxCount = 1,
|
||||
showUploadList = false,
|
||||
headers = getApiHeader(),
|
||||
action,
|
||||
onChange,
|
||||
children,
|
||||
imgCropProps,
|
||||
accept = Constants.ACCEPTED_IMAGE_FILE_TYPES,
|
||||
maxCount = 1,
|
||||
showUploadList = false,
|
||||
headers = getApiHeader(),
|
||||
action,
|
||||
onChange,
|
||||
fileType = 'image',
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
imgCropProps?: ImgCropProps;
|
||||
accept?: string;
|
||||
maxCount?: number;
|
||||
showUploadList?: boolean;
|
||||
headers?: any;
|
||||
action?: string;
|
||||
onChange?: (info: any) => void;
|
||||
children: React.ReactNode;
|
||||
imgCropProps?: ImgCropProps;
|
||||
accept?: string | string[];
|
||||
maxCount?: number;
|
||||
showUploadList?: boolean;
|
||||
headers?: any;
|
||||
action?: string;
|
||||
onChange?: (info: any) => void;
|
||||
fileType?: 'image' | 'video';
|
||||
}) {
|
||||
const beforeUpload = (file: File) => {
|
||||
if (!Constants.ACCEPTED_IMAGE_FILE_TYPES.includes(file.type)) {
|
||||
console.error("File typ not allowed!");
|
||||
return false;
|
||||
const beforeUpload = (file: File) => {
|
||||
if (!accept.includes(file.type)) {
|
||||
console.error('File typ not allowed!');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.size > Constants.MAX_IMAGE_SIZE) {
|
||||
console.error('Image is to large!');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const acceptFileTypes = Array.isArray(accept) ? accept.join(',') : (accept as string);
|
||||
|
||||
const MyUpload = () => (
|
||||
<Upload
|
||||
accept={acceptFileTypes}
|
||||
maxCount={maxCount}
|
||||
showUploadList={showUploadList}
|
||||
headers={headers}
|
||||
action={`${Constants.API_ADDRESS}${action}`}
|
||||
onChange={onChange}
|
||||
beforeUpload={beforeUpload}
|
||||
>
|
||||
{children}
|
||||
</Upload>
|
||||
);
|
||||
|
||||
if (fileType === 'video') {
|
||||
return <MyUpload />;
|
||||
}
|
||||
|
||||
if (file.size > Constants.MAX_IMAGE_SIZE) {
|
||||
console.error("Image is to large!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
return (
|
||||
<ImgCrop
|
||||
{...imgCropProps}
|
||||
rotationSlider
|
||||
>
|
||||
<Upload
|
||||
accept={accept}
|
||||
maxCount={maxCount}
|
||||
showUploadList={showUploadList}
|
||||
headers={headers}
|
||||
action={`${Constants.API_ADDRESS}${action}`}
|
||||
onChange={onChange}
|
||||
beforeUpload={beforeUpload}
|
||||
|
||||
>
|
||||
{children}
|
||||
</Upload>
|
||||
</ImgCrop>
|
||||
);
|
||||
return (
|
||||
<ImgCrop {...imgCropProps} rotationSlider>
|
||||
<MyUpload />
|
||||
</ImgCrop>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue