diff --git a/package-lock.json b/package-lock.json index 7a03a96..34825ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 4ef4101..8cd21c6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/core/components/Header/index.tsx b/src/core/components/Header/index.tsx index 6c4f6a9..afa5399 100644 --- a/src/core/components/Header/index.tsx +++ b/src/core/components/Header/index.tsx @@ -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 ( - - - - {isCollpased ? ( - dispatch(setIsSideMenuCollapsed(false))} - > - - - ) : ( - dispatch(setIsSideMenuCollapsed(true))} - > - - - )} - + + + {isCollpased ? ( + dispatch(setIsSideMenuCollapsed(false))}> + + + ) : ( + dispatch(setIsSideMenuCollapsed(true))}> + + + )} + - {props.backTo && ( - - - - Back + {props.backTo && ( + + + + Back + + + )} - - )} - - - {props.onView && ( - - - - )} + + {props.onView && ( + + + + )} - {props.onEdit && ( - - - - )} + {props.onEdit && ( + + + + )} - {isDarkMode ? ( - dispatch(setDarkMode(false))} - > - - - ) : ( - dispatch(setDarkMode(true))} - > - - - )} - } /> - - - ); + {isDarkMode ? ( + dispatch(setDarkMode(false))}> + + + ) : ( + dispatch(setDarkMode(true))}> + + + )} + , + onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS), + }, + { + key: '2', + label: 'Logout', + icon: , + danger: true, + }, + ], + }} + > + } /> + + + + ); - /* return ( + /* return ( ({ + getTeam: builder.query({ + query: () => ({ + url: 'organization/team/members', + method: 'GET', + }), + }), + getOrganizationSettings: builder.query({ + query: () => ({ + url: 'organization/settings', + method: 'GET', + }), + }), + }), +}); + +export const { useGetTeamQuery, useGetOrganizationSettingsQuery } = organizationApi; diff --git a/src/core/services/team.ts b/src/core/services/team.ts deleted file mode 100644 index d2c8d7a..0000000 --- a/src/core/services/team.ts +++ /dev/null @@ -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({ - query: () => ({ - url: "team/members", - method: "GET", - }), - }), - }), -}); - -export const { useGetTeamQuery } = teamApi; diff --git a/src/core/store/store.tsx b/src/core/store/store.tsx index dae19eb..495577e 100644 --- a/src/core/store/store.tsx +++ b/src/core/store/store.tsx @@ -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(); diff --git a/src/core/types/lesson.ts b/src/core/types/lesson.ts index 738259a..4ed7d68 100644 --- a/src/core/types/lesson.ts +++ b/src/core/types/lesson.ts @@ -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; } diff --git a/src/core/types/organization.ts b/src/core/types/organization.ts new file mode 100644 index 0000000..590e545 --- /dev/null +++ b/src/core/types/organization.ts @@ -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; +} diff --git a/src/core/types/team.ts b/src/core/types/team.ts deleted file mode 100644 index 00a559e..0000000 --- a/src/core/types/team.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface TeamMember { - Id: string; - FirstName: string; - LastName: string; - Email: string; - Role: string; - Status: string; -} diff --git a/src/core/utils/utils.tsx b/src/core/utils/utils.tsx index 36e2b57..856d3b0 100644 --- a/src/core/utils/utils.tsx +++ b/src/core/utils/utils.tsx @@ -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 { - url?: string; - method?: Method; - body?: TRequest | null; - headers?: Record; - 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; + 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({ - 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): Promise { - 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; + }); } diff --git a/src/features/AccountSettings/index.tsx b/src/features/AccountSettings/index.tsx index 59c40a9..7be651f 100644 --- a/src/features/AccountSettings/index.tsx +++ b/src/features/AccountSettings/index.tsx @@ -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 ( - <> - } - /> +export default function AccountSettings({ isAdmin }: { isAdmin?: boolean }) { + function AdminWrapper({ children }: { children: React.ReactNode }) { + if (!isAdmin) { + return <>{children}>; + } - - - - - } - title="Jorg Kreith" - description="Lead" - /> - - - - - - - - - - - - - - - - - - - - - } - htmlType="submit" - > - Update - - - - - - - > - ); -} + return ( + + {children} + + ); + } + + function TextItem({ value, name }: { value: string; name: string }) { + if (!isAdmin) { + return <>{value}>; + } + + return ( + + + + ); + } -export default function AccountSettings() { return ( <> - + */} + + + , + }, + { + key: "2", + label: "Last name", + children: , + }, + { + key: "3", + label: "Email", + children: , + }, + ]} + /> + diff --git a/src/features/Auth/SignIn/index.tsx b/src/features/Auth/SignIn/index.tsx index 5ab2cfa..88f7265 100644 --- a/src/features/Auth/SignIn/index.tsx +++ b/src/features/Auth/SignIn/index.tsx @@ -20,7 +20,7 @@ interface SignInFetchResponse { } export default function SignIn() { - const [form] = useForm(); + const [form] = useForm(); const dispatch = useDispatch(); diff --git a/src/features/Lessons/LessonPage/index.tsx b/src/features/Lessons/LessonPage/index.tsx index ad7f270..b519540 100644 --- a/src/features/Lessons/LessonPage/index.tsx +++ b/src/features/Lessons/LessonPage/index.tsx @@ -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 ; - if (error) return ; + if (isLoading) return ; + if (error) return ; - if (!data || data.length === 0) return ; + if (!data || data.length === 0) return ; - return ( - <> - {data.map((lessonContent) => ( - - - - ))} - > - ); + return ( + <> + {data.map((lessonContent) => ( + + + + ))} + > + ); }; export default function LessonPage() { - const location = useLocation(); - const navigate = useNavigate(); + const location = useLocation(); + const navigate = useNavigate(); - return ( - <> - navigate(`${location.pathname}/editor`)} - /> + return ( + <> + navigate(`${location.pathname}/editor`)} /> - - + + + + } + > + - - }> - Finish lesson - - - - > - ); + + }> + Finish lesson + + + + > + ); } diff --git a/src/features/Lessons/LessonPageEditor/Droppable.tsx b/src/features/Lessons/LessonPageEditor/Droppable.tsx index a49f2a5..86890cd 100644 --- a/src/features/Lessons/LessonPageEditor/Droppable.tsx +++ b/src/features/Lessons/LessonPageEditor/Droppable.tsx @@ -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 ( - - - - {items.map((item) => ( - - ))} - - - - ); + try { + reqUpdateLessonContentPosition({ + lessonId: currentLnId, + contentId: activeId, + newPosition: newIndex + 1, + }); + } catch (err) { + console.error(err); + } + }; + + return ( + { + setIsDragging(true); + }} + onDragEnd={handleDragEnd} + > + + + {items.map((item) => ( + + ))} + + + {isDragging ? : null} + + ); }; /* function handleDragEnd(event: DragEndEvent) { diff --git a/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx b/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx index f234cb0..e6b9a73 100644 --- a/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx +++ b/src/features/Lessons/LessonPageEditor/SortableEditorItem.tsx @@ -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 ( - - - - { - console.log("edit", lnContent.Id, data); + return ( + + + + + + + { + console.log('edit', lnContent.Id, data); - dispatch( - updateLessonContent({ - id: lnContent.Id, - data: data, - }) - ); - }} - /> + dispatch( + updateLessonContent({ + id: lnContent.Id, + data: data, + }) + ); + }} + /> + - - {component.uploadImage ? ( - - - - ) : null} - {component.uploadFileTypes ? ( - - {" "} - - ) : null} - - { - console.log("delete", lnContent.Id); - dispatch(deleteLessonContent(lnContent.Id)); + + + { + console.log('delete', lnContent.Id); + dispatch(deleteLessonContent(lnContent.Id)); - try { - reqDeleteLessonContent({ - lessonId: currentLnId, - contentId: lnContent.Id, - }); - } catch (err) { - console.error(err); - } - }} - /> - - - - - ); + try { + reqDeleteLessonContent({ + lessonId: currentLnId, + contentId: lnContent.Id, + }); + } catch (err) { + console.error(err); + } + }} + /> + + + + + ); }; +/* + {component.uploadImage ? ( + + + +) : null} +{component.uploadFileTypes ? ( + + {' '} + +) : null} +*/ + export default SortableEditorItem; diff --git a/src/features/Lessons/LessonPageEditor/index.tsx b/src/features/Lessons/LessonPageEditor/index.tsx index 61db18d..9c4ee46 100644 --- a/src/features/Lessons/LessonPageEditor/index.tsx +++ b/src/features/Lessons/LessonPageEditor/index.tsx @@ -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 ; + if (error) return ; - return ( - { - try { - const res = await updateLessonPreviewTitle({ - lessonId: lessonId as string, - newTitle: newTitle, - }).unwrap(); + return ( + { + 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 ; + if (error) return ; - return ( - - - {!lnContents || lnContents.length == 0 ? ( - - ) : ( - - )} - - - ); + return ( + + + {!lnContents || lnContents.length == 0 ? : } + + + ); }; 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 ( - <> - - navigate( - Constants.ROUTE_PATHS.LESSIONS.PAGE.replace( - ":lessonId", - lessonId as string - ) - ) - } - /> + return ( + <> + navigate(Constants.ROUTE_PATHS.LESSIONS.PAGE.replace(':lessonId', lessonId as string))} + /> - - - + + + - - - - > - ); -} \ No newline at end of file + + + + > + ); +} diff --git a/src/features/Lessons/Questions/index.tsx b/src/features/Lessons/Questions/index.tsx new file mode 100644 index 0000000..da3c706 --- /dev/null +++ b/src/features/Lessons/Questions/index.tsx @@ -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 ( + + + Questions + + + + + + Submit + + + + {questions.map((question) => ( + + ))} + + + + ); +} + +type HandleReplyFunction = (text: string, replyID?: string) => Promise; + +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 ( + + {(() => { + let nodes = []; + + for (let i = 0; i < questionsReplys.length; i++) { + if (i > showReplies - 1) { + nodes.push( + setShowReplies(showReplies + 3)} style={{ marginLeft: 64 }}> + Show more + + ); + break; + } + + nodes.push(); + } + + return nodes; + })()} + + } + 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 >} 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); + 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(null); + + return ( + <> + + + + + {user.FirstName} {user.LastName} + + {text} + + : } + 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} + > + + {likes >= 1 ? likes : ' '} + { + if (replyText === null) setReplyText(userAt); + setReplyFormVisible(!replyFormVisible); + + setTimeout(() => { + if (inputRef.current) { + const input = inputRef.current; + input.focus({ cursor: 'end' }); + } + }, 100); + }} + > + {replyFormVisible ? 'Hide' : 'Reply'} + + + {replyFormVisible ? ( + { + setIsSendingReply(true); + await onReply(replyText ? replyText : '', replyID); + + setIsSendingReply(false); + setReplyFormVisible(false); + setReplyText(null); + }} + > + + setReplyText(e.target.value)} + /> + + + + Reply + + + + ) : null} + {childContent} + + + > + ); +} diff --git a/src/features/Lessons/Questions/lessonQuestionSlice.ts b/src/features/Lessons/Questions/lessonQuestionSlice.ts new file mode 100644 index 0000000..166a138 --- /dev/null +++ b/src/features/Lessons/Questions/lessonQuestionSlice.ts @@ -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; +*/ diff --git a/src/features/Lessons/components.ts b/src/features/Lessons/components.ts index 4338cac..cb5a2ed 100644 --- a/src/features/Lessons/components.ts +++ b/src/features/Lessons/components.ts @@ -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 }; diff --git a/src/features/Lessons/converter.tsx b/src/features/Lessons/converter.tsx index eb9a8bd..f318d83 100644 --- a/src/features/Lessons/converter.tsx +++ b/src/features/Lessons/converter.tsx @@ -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); + const [reqUpdateLessonContent] = useUpdateLessonContentMutation(); - switch (lessonContent.Type) { - case getTypeByName("Header"): - if (mode === "view") { - return ( - - {lessonContent.Data} - - ); - } + const debounceRef = useRef(null); - return ( - { - 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} - - ); - case getTypeByName("Text"): - if (mode === "view") { - return ( - - {lessonContent.Data} - - ); - } - - return ( - { - console.log("edit"); - - onEdit?.(event.target.value); - - if (debounceRef.current) { - clearTimeout(debounceRef.current); + switch (lessonContent.Type) { + case getTypeByName('Header'): + if (mode === 'view') { + return {lessonContent.Data}; } - 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 ( + { + onEdit?.(event); - if (mode === "view" && lessonContent.Data === "") { - return ( - - - No image provided - - - ); - } + try { + reqUpdateLessonContent({ + lessonId: lessonId, + contentId: lessonContent.Id, + data: event, + }); + } catch (err) { + console.error(err); + } + }, + }} + level={1} + style={{ + margin: 0, + width: '100%', + }} + > + {lessonContent.Data} + + ); + case getTypeByName('Text'): + if (mode === 'view') { + const formattedText = lessonContent.Data.split('\n').map((line, index) => {line || '\u00A0'}); - if (lessonContent.Data === "") { - return ( - - - Choose image from - - - Gallery - - - - ); - } + return {formattedText}; + } - return ( - <> - - > - ); - case getTypeByName("YouTube"): - return ( - - {lessonContent.Data} - - ); - case getTypeByName("Video"): - return Not implemented; - case getTypeByName("Iframe"): - return Not implemented; - case getTypeByName("Banner"): - return Not implemented; + return ( + { + console.log('edit'); - default: - return Unknown type; - } + onEdit?.(event.target.value); + + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + + debounceRef.current = setTimeout(() => { + try { + reqUpdateLessonContent({ + lessonId: lessonId, + contentId: lessonContent.Id, + data: event.target.value, + }); + } catch (err) { + console.error(err); + } + }, 1000); + }} + /> + ); + case getTypeByName('Image'): + console.log('image', lessonContent.Data); + + if (mode === 'view' && lessonContent.Data === '') { + return ( + + + No image provided + + + ); + } + + const GalleryUpload = () => { + return ( + { + if (info.file.status === 'done') { + console.log('done'); + onEdit?.(info.file.response.Data); + } + }} + imgCropProps={{ + aspect: 5 / 4, + children: <>>, + }} + > + Gallery + + ); + }; + + if (lessonContent.Data === '') { + return ( + + + Choose image from + + + + + ); + } + + return ( + + + + {mode === 'edititable' && ( + + + Choose another image from + + + + )} + + ); + case getTypeByName('YouTube'): + const videoId = extractVideoId(lessonContent.Data); + + console.log('videoId', videoId); + + return ( + + + + {mode === 'edititable' && ( + <> + Video ID + { + 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); + }} + /> + > + )} + + ); + case getTypeByName('Video'): + if (mode === 'view' && lessonContent.Data === '') { + return ( + + + No video provided + + + ); + } + + const VideoUpload = () => { + return ( + { + if (info.file.status === 'done') { + console.log('done'); + onEdit?.(info.file.response.Data); + } + }} + accept={Constants.ACCEPTED_VIDEO_FILE_TYPES} + > + Video + + ); + }; + + if (lessonContent.Data === '') { + return ( + + + Choose video from + + + + + ); + } + + return ( + + + + + + + {mode === 'edititable' && ( + + + Choose another video from + + + + )} + + ); + + case getTypeByName('Iframe'): + return Not implemented; + case getTypeByName('Banner'): + return Not implemented; + + default: + return Unknown type; + } } diff --git a/src/features/Settings/index.tsx b/src/features/Settings/index.tsx index 47a1aff..d17c4aa 100644 --- a/src/features/Settings/index.tsx +++ b/src/features/Settings/index.tsx @@ -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(); + + 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 ; + return ( <> } /> - - - - - - Branding - - } - type="text" - shape="circle" - size="large" - htmlType="submit" - /> - - - + + } + type="text" + shape="circle" + size="large" + htmlType="submit" + /> + } + > + @@ -47,6 +80,7 @@ export default function Settings() { defaultValue="#1677ff" size="small" showText + format="hex" /> @@ -63,7 +97,11 @@ export default function Settings() { Subdomain - + @@ -128,9 +166,9 @@ export default function Settings() { /> - - - + + + > diff --git a/src/features/Team/index.tsx b/src/features/Team/index.tsx index 5bcc662..260f199 100644 --- a/src/features/Team/index.tsx +++ b/src/features/Team/index.tsx @@ -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 ; + return items; + }; - return ( - - ); + 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 ; + + return ; }; export default function Team() { - return ( - <> - } /> + return ( + <> + } /> - - - - }>Invite new member - - + + + + }>Invite new member + + - - - > - ); + + + > + ); } diff --git a/src/shared/components/MyMiddleCard/index.tsx b/src/shared/components/MyMiddleCard/index.tsx index bdb62e6..f65fb52 100644 --- a/src/shared/components/MyMiddleCard/index.tsx +++ b/src/shared/components/MyMiddleCard/index.tsx @@ -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 = ({ children, - ...props - }) => { - return ( - - - - {children} - - - - ); +interface MyMiddleCardProps extends CardProps { + outOfCardChildren?: React.ReactNode; } -export default MyMiddleCard; \ No newline at end of file +const MyMiddleCard: React.FC = ({ children, outOfCardChildren, ...props }) => { + return ( + + + + {children} + + + {outOfCardChildren} + + ); +}; + +export default MyMiddleCard; diff --git a/src/shared/components/MyUpload/index.tsx b/src/shared/components/MyUpload/index.tsx index 45df9f7..5996dc9 100644 --- a/src/shared/components/MyUpload/index.tsx +++ b/src/shared/components/MyUpload/index.tsx @@ -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 = () => ( + + {children} + + ); + + if (fileType === 'video') { + return ; } - if (file.size > Constants.MAX_IMAGE_SIZE) { - console.error("Image is to large!"); - return false; - } - - return true; - }; - - return ( - - - {children} - - - ); + return ( + + + + ); }