change subdomain
parent
8544f21b94
commit
9176e80363
|
@ -3,6 +3,8 @@ import DashboardLayout from "./core/components/DashboardLayout";
|
|||
import {
|
||||
darkMode,
|
||||
primaryColor,
|
||||
setBannerUrl,
|
||||
setLogoUrl,
|
||||
setPrimaryColor,
|
||||
setUserAuthenticated,
|
||||
userAuthenticated,
|
||||
|
@ -46,6 +48,8 @@ function App() {
|
|||
if (response) {
|
||||
dispatch(setUserAuthenticated(true));
|
||||
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
|
||||
dispatch(setLogoUrl(response.Organization.LogoUrl));
|
||||
dispatch(setBannerUrl(response.Organization.BannerUrl));
|
||||
}
|
||||
} catch (error) {}
|
||||
})();
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
lessonState,
|
||||
} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||
import { Component, componentsGroups } from "features/Lessons/components";
|
||||
import { darkMode } from "core/reducers/appSlice";
|
||||
import { darkMode, logoUrl } from "core/reducers/appSlice";
|
||||
import { DndContext, DragOverlay, useDraggable } from "@dnd-kit/core";
|
||||
import { createPortal } from "react-dom";
|
||||
import { LessonState } from "core/types/lesson";
|
||||
|
@ -43,6 +43,7 @@ export function SideMenuContent() {
|
|||
|
||||
const dispatch = useDispatch();
|
||||
const componentFirstRender = useSelector(sideMenuComponentFirstRender);
|
||||
const appLogoUrl = useSelector(logoUrl);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
@ -179,7 +180,15 @@ export function SideMenuContent() {
|
|||
}}
|
||||
></div>
|
||||
|
||||
<div style={{ overflowY: "scroll" }}>
|
||||
<div>
|
||||
<Flex justify="center" style={{ paddingBottom: 24, width: "100%" }}>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${appLogoUrl}`}
|
||||
alt="logo"
|
||||
style={{ height: 80 }}
|
||||
/>
|
||||
</Flex>
|
||||
|
||||
<Menu
|
||||
mode="inline"
|
||||
onClick={(item) => navigate(item.key)}
|
||||
|
|
|
@ -6,6 +6,8 @@ export const appSlice = createSlice({
|
|||
darkMode: false,
|
||||
userAuthenticated: null,
|
||||
primaryColor: "#1677FF",
|
||||
logoUrl: "",
|
||||
bannerUrl: "",
|
||||
},
|
||||
reducers: {
|
||||
setDarkMode: (state, action) => {
|
||||
|
@ -17,15 +19,29 @@ export const appSlice = createSlice({
|
|||
setPrimaryColor: (state, action) => {
|
||||
state.primaryColor = action.payload;
|
||||
},
|
||||
setLogoUrl: (state, action) => {
|
||||
state.logoUrl = action.payload;
|
||||
},
|
||||
setBannerUrl: (state, action) => {
|
||||
state.bannerUrl = action.payload;
|
||||
},
|
||||
},
|
||||
selectors: {
|
||||
darkMode: (state) => state.darkMode,
|
||||
userAuthenticated: (state) => state.userAuthenticated,
|
||||
primaryColor: (state) => state.primaryColor,
|
||||
logoUrl: (state) => state.logoUrl,
|
||||
bannerUrl: (state) => state.bannerUrl,
|
||||
},
|
||||
});
|
||||
|
||||
export const { setDarkMode, setUserAuthenticated, setPrimaryColor } =
|
||||
appSlice.actions;
|
||||
export const {
|
||||
setDarkMode,
|
||||
setUserAuthenticated,
|
||||
setPrimaryColor,
|
||||
setLogoUrl,
|
||||
setBannerUrl,
|
||||
} = appSlice.actions;
|
||||
|
||||
export const { darkMode, userAuthenticated, primaryColor } = appSlice.selectors;
|
||||
export const { darkMode, userAuthenticated, primaryColor, logoUrl, bannerUrl } =
|
||||
appSlice.selectors;
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import { createApi } from "@reduxjs/toolkit/query/react";
|
||||
import { baseQueryWithErrorHandling } from "core/helper/api";
|
||||
import { TeamMember, OrganizationSettings } from "core/types/organization";
|
||||
import {
|
||||
TeamMember,
|
||||
OrganizationSettings,
|
||||
IsSubdomainAvailableResponse,
|
||||
} from "core/types/organization";
|
||||
|
||||
export const organizationApi = createApi({
|
||||
reducerPath: "organizationApi",
|
||||
|
@ -25,6 +29,18 @@ export const organizationApi = createApi({
|
|||
body: { PrimaryColor: primaryColor, CompanyName: companyName },
|
||||
}),
|
||||
}),
|
||||
isSubdomainAvailable: builder.mutation({
|
||||
query: (subdomain) => ({
|
||||
url: `organization/subdomain/${subdomain}`,
|
||||
method: "GET",
|
||||
}),
|
||||
}),
|
||||
updateSubdomain: builder.mutation({
|
||||
query: (subdomain) => ({
|
||||
url: `organization/subdomain/${subdomain}`,
|
||||
method: "PATCH",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
@ -32,4 +48,6 @@ export const {
|
|||
useGetTeamQuery,
|
||||
useGetOrganizationSettingsQuery,
|
||||
useUpdateOrganizationSettingsMutation,
|
||||
useIsSubdomainAvailableMutation,
|
||||
useUpdateSubdomainMutation,
|
||||
} = organizationApi;
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
export interface TeamMember {
|
||||
Id: string;
|
||||
FirstName: string;
|
||||
LastName: string;
|
||||
Email: string;
|
||||
Role: string;
|
||||
Status: string;
|
||||
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;
|
||||
Subdomain: string;
|
||||
CompanyName: string;
|
||||
PrimaryColor: string;
|
||||
LogoUrl: string;
|
||||
BannerUrl: string;
|
||||
}
|
||||
|
||||
export interface IsSubdomainAvailableResponse {
|
||||
Available: boolean;
|
||||
}
|
||||
|
|
|
@ -1,44 +1,53 @@
|
|||
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',
|
||||
},
|
||||
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',
|
||||
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",
|
||||
},
|
||||
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'],
|
||||
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",
|
||||
],
|
||||
ACCEPTED_VIDEO_FILE_TYPES: ["video/mp4", "video/webm", "video/mkv"],
|
||||
GLOBALS: {
|
||||
MIN_SUBDOMAIN_LENGTH: 3,
|
||||
MAX_SUBDOMAIN_LENGTH: 32,
|
||||
},
|
||||
};
|
||||
|
||||
// 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
|
||||
|
@ -49,140 +58,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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,26 +1,34 @@
|
|||
import { Button, Card, Flex, Form, Input } from "antd";
|
||||
import { Button, Flex, Form, Input, Modal } from "antd";
|
||||
import HeaderBar from "../../core/components/Header";
|
||||
import MyBanner from "../../shared/components/MyBanner";
|
||||
import { MyContainer } from "../../shared/components/MyContainer";
|
||||
import { SaveOutlined } from "@ant-design/icons";
|
||||
import MyUpload from "shared/components/MyUpload";
|
||||
import { Constants } from "core/utils/utils";
|
||||
import ColorPicker from "antd/es/color-picker";
|
||||
import {
|
||||
useGetOrganizationSettingsQuery,
|
||||
useIsSubdomainAvailableMutation,
|
||||
useUpdateOrganizationSettingsMutation,
|
||||
useUpdateSubdomainMutation,
|
||||
} from "core/services/organization";
|
||||
import MyErrorResult from "shared/components/MyResult";
|
||||
import { useForm } from "antd/es/form/Form";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { AggregationColor } from "antd/es/color-picker/color";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { setPrimaryColor } from "core/reducers/appSlice";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import {
|
||||
bannerUrl,
|
||||
logoUrl,
|
||||
setBannerUrl,
|
||||
setLogoUrl,
|
||||
setPrimaryColor,
|
||||
} from "core/reducers/appSlice";
|
||||
import MyMiddleCard from "shared/components/MyMiddleCard";
|
||||
import { OrganizationSettings } from "core/types/organization";
|
||||
|
||||
type FieldType = {
|
||||
type GeneralFieldType = {
|
||||
primaryColor: string | AggregationColor;
|
||||
companyName: string;
|
||||
subdomain: string;
|
||||
};
|
||||
|
||||
export default function Settings() {
|
||||
|
@ -31,7 +39,27 @@ export default function Settings() {
|
|||
}
|
||||
);
|
||||
|
||||
const [form] = useForm<FieldType>();
|
||||
if (error) return <MyErrorResult />;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
|
||||
<GeneralCard data={data} isLoading={isLoading} />
|
||||
|
||||
<MediaCard data={data} isLoading={isLoading} />
|
||||
|
||||
<SubdomainCard data={data} isLoading={isLoading} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function GeneralCard({
|
||||
data,
|
||||
isLoading,
|
||||
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
|
||||
const [form] = useForm<GeneralFieldType>();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
||||
const currentPrimaryColor = useRef<string>();
|
||||
|
@ -39,7 +67,7 @@ export default function Settings() {
|
|||
const [updateOrganizationSettings, { isLoading: isUpdateSettingsLoading }] =
|
||||
useUpdateOrganizationSettingsMutation();
|
||||
|
||||
const handleSave = (values: FieldType) => {
|
||||
const handleSave = (values: GeneralFieldType) => {
|
||||
const hexColor =
|
||||
typeof values.primaryColor === "string"
|
||||
? values.primaryColor
|
||||
|
@ -62,7 +90,6 @@ export default function Settings() {
|
|||
form.setFieldsValue({
|
||||
primaryColor: data.PrimaryColor,
|
||||
companyName: data.CompanyName,
|
||||
subdomain: data.Subdomain,
|
||||
});
|
||||
|
||||
currentPrimaryColor.current = data.PrimaryColor;
|
||||
|
@ -79,119 +106,269 @@ export default function Settings() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
if (error) return <MyErrorResult />;
|
||||
return (
|
||||
<Form form={form} onFinish={handleSave} layout="vertical">
|
||||
<MyMiddleCard
|
||||
styles={{
|
||||
body: {
|
||||
padding: 16,
|
||||
},
|
||||
}}
|
||||
title="General"
|
||||
loading={isLoading}
|
||||
extra={
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
loading={isUpdateSettingsLoading}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex wrap gap={12}>
|
||||
<Form.Item<GeneralFieldType>
|
||||
name="primaryColor"
|
||||
label="Primary color"
|
||||
>
|
||||
<ColorPicker
|
||||
size="small"
|
||||
showText
|
||||
onChange={(color: AggregationColor) => {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
dispatch(setPrimaryColor(color.toHexString()));
|
||||
}, 600);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<GeneralFieldType> name="companyName" label="Company name">
|
||||
<Input defaultValue="Jannex" />
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</MyMiddleCard>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
function MediaCard({
|
||||
data,
|
||||
isLoading,
|
||||
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const appLogoUrl = useSelector(logoUrl);
|
||||
const appBannerUrl = useSelector(bannerUrl);
|
||||
|
||||
return (
|
||||
<Form layout="vertical">
|
||||
<MyMiddleCard title="Media" loading={isLoading}>
|
||||
<Form.Item label="Logo">
|
||||
<MyUpload
|
||||
action="/organization/file/logo"
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done" && info.file.response.Data) {
|
||||
dispatch(setLogoUrl(info.file.response.Data));
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
aspect: 1 / 1,
|
||||
children: <></>,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${appLogoUrl}`}
|
||||
alt="Company Logo"
|
||||
style={{
|
||||
width: 128,
|
||||
maxHeight: 128,
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
border: "1px solid #ddd",
|
||||
}}
|
||||
/>
|
||||
</MyUpload>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="Banner">
|
||||
<MyUpload
|
||||
action="/organization/file/banner"
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done" && info.file.response.Data) {
|
||||
dispatch(setBannerUrl(info.file.response.Data));
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
aspect: 22 / 9,
|
||||
children: <></>,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${appBannerUrl}`}
|
||||
alt="Banner"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 228,
|
||||
objectFit: "cover",
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
border: "1px solid #ddd",
|
||||
}}
|
||||
/>
|
||||
</MyUpload>
|
||||
</Form.Item>
|
||||
</MyMiddleCard>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
const subdomainPattern = /^[a-zA-Z0-9-]+$/;
|
||||
|
||||
function SubdomainCard({
|
||||
data,
|
||||
isLoading,
|
||||
}: { data?: OrganizationSettings; isLoading?: boolean } = {}) {
|
||||
const [form] = useForm();
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [reqIsSubdomainAvailable] = useIsSubdomainAvailableMutation();
|
||||
const [reqUpdateSubdomain] = useUpdateSubdomainMutation();
|
||||
|
||||
const validateSubdomain = async (rule: any, value: string) => {
|
||||
if (value) {
|
||||
if (
|
||||
value.length < Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH ||
|
||||
value.length > Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH ||
|
||||
!subdomainPattern.test(value)
|
||||
) {
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
// Check if subdomain is current subdomain
|
||||
|
||||
if (value === window.location.hostname.split(".")[0]) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await reqIsSubdomainAvailable(value);
|
||||
|
||||
if (!data.Available) {
|
||||
return Promise.reject("This subdomain is already taken!");
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
} catch (error) {
|
||||
return Promise.reject("This subdomain is already taken!");
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
form.setFieldsValue({
|
||||
subdomain: data.Subdomain,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
||||
<Modal
|
||||
title="Change Subdomain"
|
||||
open={isModalOpen}
|
||||
centered
|
||||
onCancel={() => setIsModalOpen(false)}
|
||||
okText="Change"
|
||||
onOk={() => {
|
||||
try {
|
||||
reqUpdateSubdomain(form.getFieldValue("subdomain"));
|
||||
|
||||
<MyContainer>
|
||||
<Form form={form} onFinish={handleSave} layout="vertical">
|
||||
<Card
|
||||
loading={isLoading}
|
||||
styles={{
|
||||
body: {
|
||||
padding: 16,
|
||||
},
|
||||
}}
|
||||
title="Branding"
|
||||
extra={
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
loading={isUpdateSettingsLoading}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex wrap gap={12}>
|
||||
<Form.Item<FieldType> name="primaryColor" label="Primary color">
|
||||
<ColorPicker
|
||||
size="small"
|
||||
showText
|
||||
onChange={(color: AggregationColor) => {
|
||||
if (debounceRef.current) {
|
||||
clearTimeout(debounceRef.current);
|
||||
}
|
||||
window.location.href = `https://${form.getFieldValue(
|
||||
"subdomain"
|
||||
)}.${window.location.hostname.split(".").slice(1).join(".")}`;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
Changing your subdomain will make your organization available at the
|
||||
new subdomain. Please note that you will be logged out and redirected
|
||||
to the new subdomain. Also the old subdomain will be available for
|
||||
registration by other users.
|
||||
</p>
|
||||
</Modal>
|
||||
|
||||
debounceRef.current = setTimeout(() => {
|
||||
dispatch(setPrimaryColor(color.toHexString()));
|
||||
}, 600);
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item<FieldType> name="companyName" label="Company name">
|
||||
<Input defaultValue="Jannex" />
|
||||
</Form.Item>
|
||||
<Form.Item<FieldType> name="subdomain" label="Subdomain">
|
||||
<Input
|
||||
addonBefore="https://"
|
||||
addonAfter=". jannex.de"
|
||||
defaultValue="mysite"
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="Logo">
|
||||
<MyUpload
|
||||
action={`/changeCompanyLogo`}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done") {
|
||||
//onThumbnailChanged?.();
|
||||
console.log("done");
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
aspect: 1 / 1,
|
||||
children: <></>,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
||||
alt="Company Logo"
|
||||
style={{
|
||||
width: 128,
|
||||
maxHeight: 128,
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
border: "1px solid #ddd",
|
||||
}}
|
||||
/>
|
||||
</MyUpload>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
|
||||
<Form.Item label="Thumbnail">
|
||||
<MyUpload
|
||||
action={`/changeCompanyLogo`}
|
||||
onChange={(info) => {
|
||||
if (info.file.status === "done") {
|
||||
//onThumbnailChanged?.();
|
||||
console.log("done");
|
||||
}
|
||||
}}
|
||||
imgCropProps={{
|
||||
aspect: 22 / 9,
|
||||
children: <></>,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
||||
alt="Thumbnail"
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 228,
|
||||
objectFit: "cover",
|
||||
padding: 4,
|
||||
borderRadius: 4,
|
||||
border: "1px solid #ddd",
|
||||
}}
|
||||
/>
|
||||
</MyUpload>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
requiredMark={false}
|
||||
onFinish={(values) => {
|
||||
console.log(values);
|
||||
}}
|
||||
>
|
||||
<MyMiddleCard
|
||||
title="Subdomain"
|
||||
loading={isLoading}
|
||||
extra={
|
||||
<Button
|
||||
icon={<SaveOutlined />}
|
||||
type="text"
|
||||
shape="circle"
|
||||
size="large"
|
||||
onClick={() => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => setIsModalOpen(true))
|
||||
.catch(() => {
|
||||
console.error("Validation failed!");
|
||||
});
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Flex>
|
||||
<Form.Item
|
||||
name="subdomain"
|
||||
label="Subdomain"
|
||||
hasFeedback
|
||||
validateDebounce={300}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: "Please input your subdomain!",
|
||||
},
|
||||
{
|
||||
pattern: subdomainPattern,
|
||||
message: "Please enter a valid subdomain!",
|
||||
},
|
||||
{
|
||||
min: Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH,
|
||||
message: `Subdomain must be at least ${Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH} characters!`,
|
||||
},
|
||||
{
|
||||
max: Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH,
|
||||
message: "Subdomain is too long!",
|
||||
},
|
||||
{
|
||||
required: true,
|
||||
validator: validateSubdomain,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input addonBefore="https://" addonAfter=". xx.de" />
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Form>
|
||||
</MyContainer>
|
||||
</Flex>
|
||||
</MyMiddleCard>
|
||||
</Form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { Constants } from "core/utils/utils";
|
||||
import styles from "./styles.module.css";
|
||||
import { useSelector } from "react-redux";
|
||||
import { bannerUrl } from "core/reducers/appSlice";
|
||||
|
||||
export default function MyBanner({
|
||||
title,
|
||||
|
@ -10,6 +12,8 @@ export default function MyBanner({
|
|||
subtitle?: string;
|
||||
headerBar?: React.ReactNode;
|
||||
}) {
|
||||
const appBannerUrl = useSelector(bannerUrl);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
|
@ -17,7 +21,9 @@ export default function MyBanner({
|
|||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/organization_banner.jpeg`}
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
appBannerUrl || "/demo/organization_banner.jpeg"
|
||||
}`}
|
||||
alt="banner"
|
||||
style={{
|
||||
height: 228,
|
||||
|
|
|
@ -1,67 +1,84 @@
|
|||
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';
|
||||
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, useState } from "react";
|
||||
import MySpin from "../MySpin";
|
||||
|
||||
export default function MyUpload({
|
||||
children,
|
||||
imgCropProps,
|
||||
accept = Constants.ACCEPTED_IMAGE_FILE_TYPES,
|
||||
maxCount = 1,
|
||||
showUploadList = false,
|
||||
headers = getApiHeader(),
|
||||
action,
|
||||
onChange,
|
||||
fileType = 'image',
|
||||
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 | string[];
|
||||
maxCount?: number;
|
||||
showUploadList?: boolean;
|
||||
headers?: any;
|
||||
action?: string;
|
||||
onChange?: (info: any) => void;
|
||||
fileType?: 'image' | 'video';
|
||||
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 (!accept.includes(file.type)) {
|
||||
console.error('File typ not allowed!');
|
||||
return false;
|
||||
}
|
||||
const [uploading, setUploading] = useState(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 />;
|
||||
const beforeUpload = (file: File) => {
|
||||
if (!accept.includes(file.type)) {
|
||||
console.error("File typ not allowed!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImgCrop {...imgCropProps} rotationSlider>
|
||||
<MyUpload />
|
||||
</ImgCrop>
|
||||
);
|
||||
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={(info) => {
|
||||
if (onChange) {
|
||||
console.log("call");
|
||||
onChange(info);
|
||||
}
|
||||
|
||||
if (info.file.status === "uploading") {
|
||||
setUploading(true);
|
||||
} else if (info.file.status === "done") {
|
||||
console.log("done2");
|
||||
setUploading(false);
|
||||
}
|
||||
}}
|
||||
beforeUpload={beforeUpload}
|
||||
>
|
||||
{children}
|
||||
</Upload>
|
||||
);
|
||||
|
||||
if (fileType === "video") {
|
||||
return <MyUpload />;
|
||||
}
|
||||
|
||||
return (
|
||||
<ImgCrop {...imgCropProps} rotationSlider>
|
||||
<MyUpload />
|
||||
</ImgCrop>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue