change subdomain

main
alex 2024-09-06 17:15:11 +02:00
parent 8544f21b94
commit 9176e80363
9 changed files with 574 additions and 314 deletions

View File

@ -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) {}
})(); })();

View File

@ -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)}

View File

@ -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;

View File

@ -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;

View File

@ -1,16 +1,20 @@
export interface TeamMember { export interface TeamMember {
Id: string; Id: string;
FirstName: string; FirstName: string;
LastName: string; LastName: string;
Email: string; Email: string;
Role: string; Role: string;
Status: string; Status: string;
} }
export interface OrganizationSettings { export interface OrganizationSettings {
Subdomain: string; Subdomain: string;
CompanyName: string; CompanyName: string;
PrimaryColor: string; PrimaryColor: string;
LogoUrl: string; LogoUrl: string;
BannerUrl: string; BannerUrl: string;
}
export interface IsSubdomainAvailableResponse {
Available: boolean;
} }

View File

@ -1,44 +1,53 @@
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`,
WS_ADDRESS: `${wssProtocol}${window.location.hostname}/apiws`, WS_ADDRESS: `${wssProtocol}${window.location.hostname}/apiws`,
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_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: { ORGANIZATION_TEAM: "/team",
BLACK: '#000', ORGANIZATION_TEAM_CREATE_USER: "/team/create-user",
}, ORGANIZATION_ROLES: "/roles",
MAX_IMAGE_SIZE: 25 * 1024 * 1024, // 25MB ORGANIZATION_SETTINGS: "/organization",
ACCEPTED_IMAGE_FILE_TYPES: ['image/png', 'image/jpeg', 'image/jpg', 'image/webp'], ACCOUNT_SETTINGS: "/account",
ACCEPTED_VIDEO_FILE_TYPES: ['video/mp4', 'video/webm', 'video/mkv'], 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 // used for sideMenu
export const BreakpointLgWidth = 992; export const BreakpointLgWidth = 992;
export function GetUuid() { export function GetUuid() {
return uuidv4(); return uuidv4();
} }
export function getImageUrl(imageName: string) { 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 // 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 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 = {
JSON: 0, JSON: 0,
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
t?: any; // Passen Sie dies je nach Bedarf an t?: any; // Passen Sie dies je nach Bedarf an
} }
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;
}; };
// abort fetch if it takes to long // abort fetch if it takes to long
const controller = new AbortController(); const controller = new AbortController();
const signal = controller.signal; const signal = controller.signal;
setTimeout(() => controller.abort(), 30000); // 30 seconds setTimeout(() => controller.abort(), 30000); // 30 seconds
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;
} }
return fetch(`${fetchUrl}${url}`, requestOptions as RequestInit) return fetch(`${fetchUrl}${url}`, requestOptions as RequestInit)
.then(async (response) => { .then(async (response) => {
// 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 = "/";
} }
throw response.status; throw response.status;
} }
// 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;
if (error === 401) { if (error === 401) {
if (ignoreUnauthorized) { if (ignoreUnauthorized) {
throw error; throw error;
} }
handleLogout(); handleLogout();
return; return;
} }
// show error notification for all other errors // show error notification for all other errors
if (notificationApi !== null && t !== null) { if (notificationApi !== null && t !== null) {
if (error === 500) { if (error === 500) {
/* notificationApi["error"]({ /* notificationApi["error"]({
message: t("common.request.failed.title"), message: t("common.request.failed.title"),
description: t("common.request.failed.description"), description: t("common.request.failed.description"),
}); */ }); */
} else { } else {
// other errors // other errors
/* notificationApi["error"]({ /* notificationApi["error"]({
message: t("common.request.failedInternetProblem.title"), message: t("common.request.failedInternetProblem.title"),
description: t("common.request.failedInternetProblem.description"), description: t("common.request.failedInternetProblem.description"),
}); */ }); */
} }
} }
throw error; throw error;
}); });
} }

View File

@ -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,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 ( 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> window.location.href = `https://${form.getFieldValue(
<Form form={form} onFinish={handleSave} layout="vertical"> "subdomain"
<Card )}.${window.location.hostname.split(".").slice(1).join(".")}`;
loading={isLoading} } catch (error) {
styles={{ console.error(error);
body: { }
padding: 16, }}
}, >
}} <p>
title="Branding" Changing your subdomain will make your organization available at the
extra={ new subdomain. Please note that you will be logged out and redirected
<Button to the new subdomain. Also the old subdomain will be available for
icon={<SaveOutlined />} registration by other users.
type="text" </p>
shape="circle" </Modal>
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);
}
debounceRef.current = setTimeout(() => { <Form
dispatch(setPrimaryColor(color.toHexString())); form={form}
}, 600); layout="vertical"
}} requiredMark={false}
/> onFinish={(values) => {
</Form.Item> console.log(values);
<Form.Item<FieldType> name="companyName" label="Company name"> }}
<Input defaultValue="Jannex" /> >
</Form.Item> <MyMiddleCard
<Form.Item<FieldType> name="subdomain" label="Subdomain"> title="Subdomain"
<Input loading={isLoading}
addonBefore="https://" extra={
addonAfter=". jannex.de" <Button
defaultValue="mysite" icon={<SaveOutlined />}
/> type="text"
</Form.Item> shape="circle"
<Form.Item label="Logo"> size="large"
<MyUpload onClick={() => {
action={`/changeCompanyLogo`} form
onChange={(info) => { .validateFields()
if (info.file.status === "done") { .then(() => setIsModalOpen(true))
//onThumbnailChanged?.(); .catch(() => {
console.log("done"); console.error("Validation failed!");
} });
}} }}
imgCropProps={{ />
aspect: 1 / 1, }
children: <></>, >
}} <Flex>
> <Form.Item
<img name="subdomain"
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`} label="Subdomain"
alt="Company Logo" hasFeedback
style={{ validateDebounce={300}
width: 128, rules={[
maxHeight: 128, {
padding: 4, required: true,
borderRadius: 4, message: "Please input your subdomain!",
border: "1px solid #ddd", },
}} {
/> pattern: subdomainPattern,
</MyUpload> message: "Please enter a valid subdomain!",
</Form.Item> },
</Flex> {
min: Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH,
<Form.Item label="Thumbnail"> message: `Subdomain must be at least ${Constants.GLOBALS.MIN_SUBDOMAIN_LENGTH} characters!`,
<MyUpload },
action={`/changeCompanyLogo`} {
onChange={(info) => { max: Constants.GLOBALS.MAX_SUBDOMAIN_LENGTH,
if (info.file.status === "done") { message: "Subdomain is too long!",
//onThumbnailChanged?.(); },
console.log("done"); {
} required: true,
}} validator: validateSubdomain,
imgCropProps={{ },
aspect: 22 / 9, ]}
children: <></>, >
}} <Input addonBefore="https://" addonAfter=". xx.de" />
>
<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.Item> </Form.Item>
</Card> </Flex>
</Form> </MyMiddleCard>
</MyContainer> </Form>
</> </>
); );
} }

View File

@ -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,

View File

@ -1,67 +1,84 @@
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,
imgCropProps, imgCropProps,
accept = Constants.ACCEPTED_IMAGE_FILE_TYPES, accept = Constants.ACCEPTED_IMAGE_FILE_TYPES,
maxCount = 1, maxCount = 1,
showUploadList = false, showUploadList = false,
headers = getApiHeader(), headers = getApiHeader(),
action, action,
onChange, onChange,
fileType = 'image', fileType = "image",
}: { }: {
children: React.ReactNode; children: React.ReactNode;
imgCropProps?: ImgCropProps; imgCropProps?: ImgCropProps;
accept?: string | string[]; accept?: string | string[];
maxCount?: number; maxCount?: number;
showUploadList?: boolean; showUploadList?: boolean;
headers?: any; headers?: any;
action?: string; action?: string;
onChange?: (info: any) => void; onChange?: (info: any) => void;
fileType?: 'image' | 'video'; fileType?: "image" | "video";
}) { }) {
const beforeUpload = (file: File) => { const [uploading, setUploading] = useState(false);
if (!accept.includes(file.type)) {
console.error('File typ not allowed!');
return false;
}
if (file.size > Constants.MAX_IMAGE_SIZE) { const beforeUpload = (file: File) => {
console.error('Image is to large!'); if (!accept.includes(file.type)) {
return false; console.error("File typ not allowed!");
} 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 />;
} }
return ( if (file.size > Constants.MAX_IMAGE_SIZE) {
<ImgCrop {...imgCropProps} rotationSlider> console.error("Image is to large!");
<MyUpload /> return false;
</ImgCrop> }
);
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>
);
} }