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

View File

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

View File

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

View File

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

View File

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

View File

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

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 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>
</>
);
}

View File

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

View File

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