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

@ -14,3 +14,7 @@ export interface OrganizationSettings {
LogoUrl: string; LogoUrl: string;
BannerUrl: string; BannerUrl: string;
} }
export interface IsSubdomainAvailableResponse {
Available: boolean;
}

View File

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

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

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