change subdomain
parent
8544f21b94
commit
9176e80363
|
@ -3,6 +3,8 @@ import DashboardLayout from "./core/components/DashboardLayout";
|
||||||
import {
|
import {
|
||||||
darkMode,
|
darkMode,
|
||||||
primaryColor,
|
primaryColor,
|
||||||
|
setBannerUrl,
|
||||||
|
setLogoUrl,
|
||||||
setPrimaryColor,
|
setPrimaryColor,
|
||||||
setUserAuthenticated,
|
setUserAuthenticated,
|
||||||
userAuthenticated,
|
userAuthenticated,
|
||||||
|
@ -46,6 +48,8 @@ function App() {
|
||||||
if (response) {
|
if (response) {
|
||||||
dispatch(setUserAuthenticated(true));
|
dispatch(setUserAuthenticated(true));
|
||||||
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
|
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
|
||||||
|
dispatch(setLogoUrl(response.Organization.LogoUrl));
|
||||||
|
dispatch(setBannerUrl(response.Organization.BannerUrl));
|
||||||
}
|
}
|
||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
lessonState,
|
lessonState,
|
||||||
} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice";
|
||||||
import { Component, componentsGroups } from "features/Lessons/components";
|
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 { DndContext, DragOverlay, useDraggable } from "@dnd-kit/core";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { LessonState } from "core/types/lesson";
|
import { LessonState } from "core/types/lesson";
|
||||||
|
@ -43,6 +43,7 @@ export function SideMenuContent() {
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const componentFirstRender = useSelector(sideMenuComponentFirstRender);
|
const componentFirstRender = useSelector(sideMenuComponentFirstRender);
|
||||||
|
const appLogoUrl = useSelector(logoUrl);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
@ -179,7 +180,15 @@ export function SideMenuContent() {
|
||||||
}}
|
}}
|
||||||
></div>
|
></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
|
<Menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
onClick={(item) => navigate(item.key)}
|
onClick={(item) => navigate(item.key)}
|
||||||
|
|
|
@ -6,6 +6,8 @@ export const appSlice = createSlice({
|
||||||
darkMode: false,
|
darkMode: false,
|
||||||
userAuthenticated: null,
|
userAuthenticated: null,
|
||||||
primaryColor: "#1677FF",
|
primaryColor: "#1677FF",
|
||||||
|
logoUrl: "",
|
||||||
|
bannerUrl: "",
|
||||||
},
|
},
|
||||||
reducers: {
|
reducers: {
|
||||||
setDarkMode: (state, action) => {
|
setDarkMode: (state, action) => {
|
||||||
|
@ -17,15 +19,29 @@ export const appSlice = createSlice({
|
||||||
setPrimaryColor: (state, action) => {
|
setPrimaryColor: (state, action) => {
|
||||||
state.primaryColor = action.payload;
|
state.primaryColor = action.payload;
|
||||||
},
|
},
|
||||||
|
setLogoUrl: (state, action) => {
|
||||||
|
state.logoUrl = action.payload;
|
||||||
|
},
|
||||||
|
setBannerUrl: (state, action) => {
|
||||||
|
state.bannerUrl = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
darkMode: (state) => state.darkMode,
|
darkMode: (state) => state.darkMode,
|
||||||
userAuthenticated: (state) => state.userAuthenticated,
|
userAuthenticated: (state) => state.userAuthenticated,
|
||||||
primaryColor: (state) => state.primaryColor,
|
primaryColor: (state) => state.primaryColor,
|
||||||
|
logoUrl: (state) => state.logoUrl,
|
||||||
|
bannerUrl: (state) => state.bannerUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setDarkMode, setUserAuthenticated, setPrimaryColor } =
|
export const {
|
||||||
appSlice.actions;
|
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 { createApi } from "@reduxjs/toolkit/query/react";
|
||||||
import { baseQueryWithErrorHandling } from "core/helper/api";
|
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({
|
export const organizationApi = createApi({
|
||||||
reducerPath: "organizationApi",
|
reducerPath: "organizationApi",
|
||||||
|
@ -25,6 +29,18 @@ export const organizationApi = createApi({
|
||||||
body: { PrimaryColor: primaryColor, CompanyName: companyName },
|
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,
|
useGetTeamQuery,
|
||||||
useGetOrganizationSettingsQuery,
|
useGetOrganizationSettingsQuery,
|
||||||
useUpdateOrganizationSettingsMutation,
|
useUpdateOrganizationSettingsMutation,
|
||||||
|
useIsSubdomainAvailableMutation,
|
||||||
|
useUpdateSubdomainMutation,
|
||||||
} = organizationApi;
|
} = organizationApi;
|
||||||
|
|
|
@ -14,3 +14,7 @@ export interface OrganizationSettings {
|
||||||
LogoUrl: string;
|
LogoUrl: string;
|
||||||
BannerUrl: string;
|
BannerUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IsSubdomainAvailableResponse {
|
||||||
|
Available: boolean;
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from "buffer";
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
const wssProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
|
const wssProtocol = window.location.protocol === "https:" ? "wss://" : "ws://";
|
||||||
|
|
||||||
export const Constants = {
|
export const Constants = {
|
||||||
API_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/v1`,
|
API_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/v1`,
|
||||||
|
@ -9,25 +9,34 @@ export const Constants = {
|
||||||
STATIC_CONTENT_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/static`,
|
STATIC_CONTENT_ADDRESS: `${window.location.protocol}//${window.location.hostname}/api/static`,
|
||||||
ROUTE_PATHS: {
|
ROUTE_PATHS: {
|
||||||
LESSIONS: {
|
LESSIONS: {
|
||||||
ROOT: '/lessons',
|
ROOT: "/lessons",
|
||||||
PAGE: '/lessons/:lessonId',
|
PAGE: "/lessons/:lessonId",
|
||||||
PAGE_EDITOR: '/lessons/:lessonId/editor',
|
PAGE_EDITOR: "/lessons/:lessonId/editor",
|
||||||
},
|
},
|
||||||
ORGANIZATION_TEAM: '/team',
|
ORGANIZATION_TEAM: "/team",
|
||||||
ORGANIZATION_TEAM_CREATE_USER: '/team/create-user',
|
ORGANIZATION_TEAM_CREATE_USER: "/team/create-user",
|
||||||
ORGANIZATION_ROLES: '/roles',
|
ORGANIZATION_ROLES: "/roles",
|
||||||
ORGANIZATION_SETTINGS: '/organization',
|
ORGANIZATION_SETTINGS: "/organization",
|
||||||
ACCOUNT_SETTINGS: '/account',
|
ACCOUNT_SETTINGS: "/account",
|
||||||
WHATS_NEW: '/whats-new',
|
WHATS_NEW: "/whats-new",
|
||||||
SUGGEST_FEATURE: '/suggest-feature',
|
SUGGEST_FEATURE: "/suggest-feature",
|
||||||
CONTACT_SUPPORT: '/contact-support',
|
CONTACT_SUPPORT: "/contact-support",
|
||||||
},
|
},
|
||||||
STYLES: {
|
STYLES: {
|
||||||
BLACK: '#000',
|
BLACK: "#000",
|
||||||
},
|
},
|
||||||
MAX_IMAGE_SIZE: 25 * 1024 * 1024, // 25MB
|
MAX_IMAGE_SIZE: 25 * 1024 * 1024, // 25MB
|
||||||
ACCEPTED_IMAGE_FILE_TYPES: ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'],
|
ACCEPTED_IMAGE_FILE_TYPES: [
|
||||||
ACCEPTED_VIDEO_FILE_TYPES: ['video/mp4', 'video/webm', 'video/mkv'],
|
"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
|
// used for sideMenu
|
||||||
|
@ -49,20 +58,20 @@ export function getImageUrl(imageName: string) {
|
||||||
export const BrowserTabSession = GetUuid();
|
export const BrowserTabSession = GetUuid();
|
||||||
|
|
||||||
export function getUserSessionFromLocalStorage() {
|
export function getUserSessionFromLocalStorage() {
|
||||||
return localStorage.getItem('session');
|
return localStorage.getItem("session");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EncodeStringToBase64(value: string) {
|
export function EncodeStringToBase64(value: string) {
|
||||||
return Buffer.from(value).toString('base64');
|
return Buffer.from(value).toString("base64");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DecodedBase64ToString(value: string) {
|
export function DecodedBase64ToString(value: string) {
|
||||||
return Buffer.from(value, 'base64').toString();
|
return Buffer.from(value, "base64").toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function handleLogout() {
|
export function handleLogout() {
|
||||||
localStorage.removeItem('session');
|
localStorage.removeItem("session");
|
||||||
window.location.href = '/';
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const myFetchContentType = {
|
export const myFetchContentType = {
|
||||||
|
@ -70,14 +79,14 @@ export const myFetchContentType = {
|
||||||
FORM_DATA: 1,
|
FORM_DATA: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||||
|
|
||||||
interface MyFetchOptions<TRequest = any, TResponse = any> {
|
interface MyFetchOptions<TRequest = any, TResponse = any> {
|
||||||
url?: string;
|
url?: string;
|
||||||
method?: Method;
|
method?: Method;
|
||||||
body?: TRequest | null;
|
body?: TRequest | null;
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
contentType?: 'JSON' | 'FORM_DATA';
|
contentType?: "JSON" | "FORM_DATA";
|
||||||
fetchUrl?: string;
|
fetchUrl?: string;
|
||||||
ignoreUnauthorized?: boolean;
|
ignoreUnauthorized?: boolean;
|
||||||
notificationApi?: any; // Passen Sie dies je nach Bedarf an
|
notificationApi?: any; // Passen Sie dies je nach Bedarf an
|
||||||
|
@ -85,26 +94,26 @@ interface MyFetchOptions<TRequest = any, TResponse = any> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function myFetch<TRequest = any, TResponse = any>({
|
export function myFetch<TRequest = any, TResponse = any>({
|
||||||
url = '',
|
url = "",
|
||||||
method = 'GET',
|
method = "GET",
|
||||||
body = null,
|
body = null,
|
||||||
headers = {},
|
headers = {},
|
||||||
contentType = 'JSON',
|
contentType = "JSON",
|
||||||
fetchUrl = Constants.API_ADDRESS,
|
fetchUrl = Constants.API_ADDRESS,
|
||||||
ignoreUnauthorized = false,
|
ignoreUnauthorized = false,
|
||||||
notificationApi = null,
|
notificationApi = null,
|
||||||
t = null,
|
t = null,
|
||||||
}: MyFetchOptions<TRequest, TResponse>): Promise<TResponse> {
|
}: MyFetchOptions<TRequest, TResponse>): Promise<TResponse> {
|
||||||
const getContentType = () => {
|
const getContentType = () => {
|
||||||
if (contentType === 'JSON') return 'application/json';
|
if (contentType === "JSON") return "application/json";
|
||||||
|
|
||||||
return 'multipart/form-data';
|
return "multipart/form-data";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getBody = () => {
|
const getBody = () => {
|
||||||
if (!body) return null;
|
if (!body) return null;
|
||||||
|
|
||||||
if (contentType === 'JSON') return JSON.stringify(body);
|
if (contentType === "JSON") return JSON.stringify(body);
|
||||||
|
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
|
@ -118,15 +127,15 @@ export function myFetch<TRequest = any, TResponse = any>({
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
'X-Authorization': getUserSessionFromLocalStorage() || '',
|
"X-Authorization": getUserSessionFromLocalStorage() || "",
|
||||||
'Content-Type': getContentType(),
|
"Content-Type": getContentType(),
|
||||||
...headers,
|
...headers,
|
||||||
},
|
},
|
||||||
body: getBody(),
|
body: getBody(),
|
||||||
signal: signal,
|
signal: signal,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (fetchUrl === '') {
|
if (fetchUrl === "") {
|
||||||
fetchUrl = Constants.API_ADDRESS;
|
fetchUrl = Constants.API_ADDRESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -135,7 +144,7 @@ export function myFetch<TRequest = any, TResponse = any>({
|
||||||
// if status is not in range 200-299
|
// if status is not in range 200-299
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (!ignoreUnauthorized && response.status === 401) {
|
if (!ignoreUnauthorized && response.status === 401) {
|
||||||
console.error('Unauthorized');
|
console.error("Unauthorized");
|
||||||
// TODO: check here
|
// TODO: check here
|
||||||
//setUserSessionToLocalStorage("");
|
//setUserSessionToLocalStorage("");
|
||||||
//window.location.href = "/";
|
//window.location.href = "/";
|
||||||
|
@ -145,14 +154,14 @@ export function myFetch<TRequest = any, TResponse = any>({
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if response is json
|
// check if response is json
|
||||||
if (response.headers.get('content-type')?.includes('application/json')) {
|
if (response.headers.get("content-type")?.includes("application/json")) {
|
||||||
return response.json();
|
return response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.text();
|
return response.text();
|
||||||
})
|
})
|
||||||
.catch(async (error) => {
|
.catch(async (error) => {
|
||||||
console.error('Error', error);
|
console.error("Error", error);
|
||||||
|
|
||||||
// ignore errors here as they are handled in the components
|
// ignore errors here as they are handled in the components
|
||||||
if (error === 400) throw error;
|
if (error === 400) throw error;
|
||||||
|
|
|
@ -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 HeaderBar from "../../core/components/Header";
|
||||||
import MyBanner from "../../shared/components/MyBanner";
|
import MyBanner from "../../shared/components/MyBanner";
|
||||||
import { MyContainer } from "../../shared/components/MyContainer";
|
|
||||||
import { SaveOutlined } from "@ant-design/icons";
|
import { SaveOutlined } from "@ant-design/icons";
|
||||||
import MyUpload from "shared/components/MyUpload";
|
import MyUpload from "shared/components/MyUpload";
|
||||||
import { Constants } from "core/utils/utils";
|
import { Constants } from "core/utils/utils";
|
||||||
import ColorPicker from "antd/es/color-picker";
|
import ColorPicker from "antd/es/color-picker";
|
||||||
import {
|
import {
|
||||||
useGetOrganizationSettingsQuery,
|
useGetOrganizationSettingsQuery,
|
||||||
|
useIsSubdomainAvailableMutation,
|
||||||
useUpdateOrganizationSettingsMutation,
|
useUpdateOrganizationSettingsMutation,
|
||||||
|
useUpdateSubdomainMutation,
|
||||||
} from "core/services/organization";
|
} from "core/services/organization";
|
||||||
import MyErrorResult from "shared/components/MyResult";
|
import MyErrorResult from "shared/components/MyResult";
|
||||||
import { useForm } from "antd/es/form/Form";
|
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 { AggregationColor } from "antd/es/color-picker/color";
|
||||||
import { useDispatch } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { setPrimaryColor } from "core/reducers/appSlice";
|
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;
|
primaryColor: string | AggregationColor;
|
||||||
companyName: string;
|
companyName: string;
|
||||||
subdomain: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Settings() {
|
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 dispatch = useDispatch();
|
||||||
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
const debounceRef = useRef<null | NodeJS.Timeout>(null);
|
||||||
const currentPrimaryColor = useRef<string>();
|
const currentPrimaryColor = useRef<string>();
|
||||||
|
@ -39,7 +67,7 @@ export default function Settings() {
|
||||||
const [updateOrganizationSettings, { isLoading: isUpdateSettingsLoading }] =
|
const [updateOrganizationSettings, { isLoading: isUpdateSettingsLoading }] =
|
||||||
useUpdateOrganizationSettingsMutation();
|
useUpdateOrganizationSettingsMutation();
|
||||||
|
|
||||||
const handleSave = (values: FieldType) => {
|
const handleSave = (values: GeneralFieldType) => {
|
||||||
const hexColor =
|
const hexColor =
|
||||||
typeof values.primaryColor === "string"
|
typeof values.primaryColor === "string"
|
||||||
? values.primaryColor
|
? values.primaryColor
|
||||||
|
@ -62,7 +90,6 @@ export default function Settings() {
|
||||||
form.setFieldsValue({
|
form.setFieldsValue({
|
||||||
primaryColor: data.PrimaryColor,
|
primaryColor: data.PrimaryColor,
|
||||||
companyName: data.CompanyName,
|
companyName: data.CompanyName,
|
||||||
subdomain: data.Subdomain,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
currentPrimaryColor.current = data.PrimaryColor;
|
currentPrimaryColor.current = data.PrimaryColor;
|
||||||
|
@ -79,22 +106,16 @@ export default function Settings() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (error) return <MyErrorResult />;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
|
|
||||||
|
|
||||||
<MyContainer>
|
|
||||||
<Form form={form} onFinish={handleSave} layout="vertical">
|
<Form form={form} onFinish={handleSave} layout="vertical">
|
||||||
<Card
|
<MyMiddleCard
|
||||||
loading={isLoading}
|
|
||||||
styles={{
|
styles={{
|
||||||
body: {
|
body: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
title="Branding"
|
title="General"
|
||||||
|
loading={isLoading}
|
||||||
extra={
|
extra={
|
||||||
<Button
|
<Button
|
||||||
icon={<SaveOutlined />}
|
icon={<SaveOutlined />}
|
||||||
|
@ -107,7 +128,10 @@ export default function Settings() {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Flex wrap gap={12}>
|
<Flex wrap gap={12}>
|
||||||
<Form.Item<FieldType> name="primaryColor" label="Primary color">
|
<Form.Item<GeneralFieldType>
|
||||||
|
name="primaryColor"
|
||||||
|
label="Primary color"
|
||||||
|
>
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
size="small"
|
size="small"
|
||||||
showText
|
showText
|
||||||
|
@ -122,23 +146,34 @@ export default function Settings() {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FieldType> name="companyName" label="Company name">
|
|
||||||
|
<Form.Item<GeneralFieldType> name="companyName" label="Company name">
|
||||||
<Input defaultValue="Jannex" />
|
<Input defaultValue="Jannex" />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item<FieldType> name="subdomain" label="Subdomain">
|
</Flex>
|
||||||
<Input
|
</MyMiddleCard>
|
||||||
addonBefore="https://"
|
</Form>
|
||||||
addonAfter=". jannex.de"
|
);
|
||||||
defaultValue="mysite"
|
}
|
||||||
/>
|
|
||||||
</Form.Item>
|
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">
|
<Form.Item label="Logo">
|
||||||
<MyUpload
|
<MyUpload
|
||||||
action={`/changeCompanyLogo`}
|
action="/organization/file/logo"
|
||||||
onChange={(info) => {
|
onChange={(info) => {
|
||||||
if (info.file.status === "done") {
|
if (info.file.status === "done" && info.file.response.Data) {
|
||||||
//onThumbnailChanged?.();
|
dispatch(setLogoUrl(info.file.response.Data));
|
||||||
console.log("done");
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
imgCropProps={{
|
imgCropProps={{
|
||||||
|
@ -147,7 +182,7 @@ export default function Settings() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
src={`${Constants.STATIC_CONTENT_ADDRESS}${appLogoUrl}`}
|
||||||
alt="Company Logo"
|
alt="Company Logo"
|
||||||
style={{
|
style={{
|
||||||
width: 128,
|
width: 128,
|
||||||
|
@ -159,15 +194,13 @@ export default function Settings() {
|
||||||
/>
|
/>
|
||||||
</MyUpload>
|
</MyUpload>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Flex>
|
|
||||||
|
|
||||||
<Form.Item label="Thumbnail">
|
<Form.Item label="Banner">
|
||||||
<MyUpload
|
<MyUpload
|
||||||
action={`/changeCompanyLogo`}
|
action="/organization/file/banner"
|
||||||
onChange={(info) => {
|
onChange={(info) => {
|
||||||
if (info.file.status === "done") {
|
if (info.file.status === "done" && info.file.response.Data) {
|
||||||
//onThumbnailChanged?.();
|
dispatch(setBannerUrl(info.file.response.Data));
|
||||||
console.log("done");
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
imgCropProps={{
|
imgCropProps={{
|
||||||
|
@ -176,8 +209,8 @@ export default function Settings() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
|
src={`${Constants.STATIC_CONTENT_ADDRESS}${appBannerUrl}`}
|
||||||
alt="Thumbnail"
|
alt="Banner"
|
||||||
style={{
|
style={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 228,
|
height: 228,
|
||||||
|
@ -189,9 +222,153 @@ export default function Settings() {
|
||||||
/>
|
/>
|
||||||
</MyUpload>
|
</MyUpload>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Card>
|
</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 (
|
||||||
|
<>
|
||||||
|
<Modal
|
||||||
|
title="Change Subdomain"
|
||||||
|
open={isModalOpen}
|
||||||
|
centered
|
||||||
|
onCancel={() => setIsModalOpen(false)}
|
||||||
|
okText="Change"
|
||||||
|
onOk={() => {
|
||||||
|
try {
|
||||||
|
reqUpdateSubdomain(form.getFieldValue("subdomain"));
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</Flex>
|
||||||
|
</MyMiddleCard>
|
||||||
</Form>
|
</Form>
|
||||||
</MyContainer>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { Constants } from "core/utils/utils";
|
import { Constants } from "core/utils/utils";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { bannerUrl } from "core/reducers/appSlice";
|
||||||
|
|
||||||
export default function MyBanner({
|
export default function MyBanner({
|
||||||
title,
|
title,
|
||||||
|
@ -10,6 +12,8 @@ export default function MyBanner({
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
headerBar?: React.ReactNode;
|
headerBar?: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
|
const appBannerUrl = useSelector(bannerUrl);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -17,7 +21,9 @@ export default function MyBanner({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/organization_banner.jpeg`}
|
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||||
|
appBannerUrl || "/demo/organization_banner.jpeg"
|
||||||
|
}`}
|
||||||
alt="banner"
|
alt="banner"
|
||||||
style={{
|
style={{
|
||||||
height: 228,
|
height: 228,
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import ImgCrop, { ImgCropProps } from 'antd-img-crop';
|
import ImgCrop, { ImgCropProps } from "antd-img-crop";
|
||||||
import Upload from 'antd/es/upload/Upload';
|
import Upload from "antd/es/upload/Upload";
|
||||||
import { getApiHeader } from 'core/helper/api';
|
import { getApiHeader } from "core/helper/api";
|
||||||
import { Constants } from 'core/utils/utils';
|
import { Constants } from "core/utils/utils";
|
||||||
import { Fragment } from 'react';
|
import { Fragment, useState } from "react";
|
||||||
|
import MySpin from "../MySpin";
|
||||||
|
|
||||||
export default function MyUpload({
|
export default function MyUpload({
|
||||||
children,
|
children,
|
||||||
|
@ -13,7 +14,7 @@ export default function MyUpload({
|
||||||
headers = getApiHeader(),
|
headers = getApiHeader(),
|
||||||
action,
|
action,
|
||||||
onChange,
|
onChange,
|
||||||
fileType = 'image',
|
fileType = "image",
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
imgCropProps?: ImgCropProps;
|
imgCropProps?: ImgCropProps;
|
||||||
|
@ -23,23 +24,27 @@ export default function MyUpload({
|
||||||
headers?: any;
|
headers?: any;
|
||||||
action?: string;
|
action?: string;
|
||||||
onChange?: (info: any) => void;
|
onChange?: (info: any) => void;
|
||||||
fileType?: 'image' | 'video';
|
fileType?: "image" | "video";
|
||||||
}) {
|
}) {
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
|
||||||
const beforeUpload = (file: File) => {
|
const beforeUpload = (file: File) => {
|
||||||
if (!accept.includes(file.type)) {
|
if (!accept.includes(file.type)) {
|
||||||
console.error('File typ not allowed!');
|
console.error("File typ not allowed!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.size > Constants.MAX_IMAGE_SIZE) {
|
if (file.size > Constants.MAX_IMAGE_SIZE) {
|
||||||
console.error('Image is to large!');
|
console.error("Image is to large!");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const acceptFileTypes = Array.isArray(accept) ? accept.join(',') : (accept as string);
|
const acceptFileTypes = Array.isArray(accept)
|
||||||
|
? accept.join(",")
|
||||||
|
: (accept as string);
|
||||||
|
|
||||||
const MyUpload = () => (
|
const MyUpload = () => (
|
||||||
<Upload
|
<Upload
|
||||||
|
@ -48,14 +53,26 @@ export default function MyUpload({
|
||||||
showUploadList={showUploadList}
|
showUploadList={showUploadList}
|
||||||
headers={headers}
|
headers={headers}
|
||||||
action={`${Constants.API_ADDRESS}${action}`}
|
action={`${Constants.API_ADDRESS}${action}`}
|
||||||
onChange={onChange}
|
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}
|
beforeUpload={beforeUpload}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</Upload>
|
</Upload>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (fileType === 'video') {
|
if (fileType === "video") {
|
||||||
return <MyUpload />;
|
return <MyUpload />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue