update settings

main
alex 2024-09-04 22:56:58 +02:00
parent e6b9a6d0e8
commit 8544f21b94
4 changed files with 195 additions and 150 deletions

View File

@ -2,6 +2,8 @@ import { ConfigProvider, Layout, theme } from "antd";
import DashboardLayout from "./core/components/DashboardLayout"; import DashboardLayout from "./core/components/DashboardLayout";
import { import {
darkMode, darkMode,
primaryColor,
setPrimaryColor,
setUserAuthenticated, setUserAuthenticated,
userAuthenticated, userAuthenticated,
} from "./core/reducers/appSlice"; } from "./core/reducers/appSlice";
@ -19,6 +21,7 @@ function App() {
const isDarkMode = useSelector(darkMode); const isDarkMode = useSelector(darkMode);
const uAuthenticated = useSelector(userAuthenticated); const uAuthenticated = useSelector(userAuthenticated);
const primColor = useSelector(primaryColor);
console.info( console.info(
"\n %c LMS %c v1.0.0 %c \n", "\n %c LMS %c v1.0.0 %c \n",
@ -36,12 +39,13 @@ function App() {
(async () => { (async () => {
try { try {
const response = await myFetch({ const response = await myFetch({
url: "/user", url: "/app",
method: "GET", method: "GET",
}); });
if (response) { if (response) {
dispatch(setUserAuthenticated(true)) dispatch(setUserAuthenticated(true));
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
} }
} catch (error) {} } catch (error) {}
})(); })();
@ -58,10 +62,13 @@ function App() {
<ConfigProvider <ConfigProvider
theme={{ theme={{
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
token: {
colorPrimary: primColor,
},
}} }}
> >
{uAuthenticated == null ? ( {uAuthenticated == null ? (
<MyCenteredSpin /> <MyCenteredSpin />
) : uAuthenticated ? ( ) : uAuthenticated ? (
<DashboardLayout /> <DashboardLayout />
) : ( ) : (

View File

@ -5,6 +5,7 @@ export const appSlice = createSlice({
initialState: { initialState: {
darkMode: false, darkMode: false,
userAuthenticated: null, userAuthenticated: null,
primaryColor: "#1677FF",
}, },
reducers: { reducers: {
setDarkMode: (state, action) => { setDarkMode: (state, action) => {
@ -12,14 +13,19 @@ export const appSlice = createSlice({
}, },
setUserAuthenticated: (state, action) => { setUserAuthenticated: (state, action) => {
state.userAuthenticated = action.payload; state.userAuthenticated = action.payload;
} },
setPrimaryColor: (state, action) => {
state.primaryColor = action.payload;
},
}, },
selectors: { selectors: {
darkMode: (state) => state.darkMode, darkMode: (state) => state.darkMode,
userAuthenticated: (state) => state.userAuthenticated, userAuthenticated: (state) => state.userAuthenticated,
primaryColor: (state) => state.primaryColor,
}, },
}) });
export const { setDarkMode, setUserAuthenticated } = appSlice.actions; export const { setDarkMode, setUserAuthenticated, setPrimaryColor } =
appSlice.actions;
export const { darkMode, userAuthenticated } = appSlice.selectors; export const { darkMode, userAuthenticated, primaryColor } = appSlice.selectors;

View File

@ -1,24 +1,35 @@
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 } from "core/types/organization";
export const organizationApi = createApi({ export const organizationApi = createApi({
reducerPath: 'organizationApi', reducerPath: "organizationApi",
baseQuery: baseQueryWithErrorHandling, baseQuery: baseQueryWithErrorHandling,
endpoints: (builder) => ({ endpoints: (builder) => ({
getTeam: builder.query<TeamMember[], undefined>({ getTeam: builder.query<TeamMember[], undefined>({
query: () => ({ query: () => ({
url: 'organization/team/members', url: "organization/team/members",
method: 'GET', method: "GET",
}), }),
}),
getOrganizationSettings: builder.query<OrganizationSettings, undefined>({
query: () => ({
url: 'organization/settings',
method: 'GET',
}),
}),
}), }),
getOrganizationSettings: builder.query<OrganizationSettings, undefined>({
query: () => ({
url: "organization/settings",
method: "GET",
}),
}),
updateOrganizationSettings: builder.mutation({
query: ({ primaryColor, companyName }) => ({
url: "organization/settings",
method: "PATCH",
body: { PrimaryColor: primaryColor, CompanyName: companyName },
}),
}),
}),
}); });
export const { useGetTeamQuery, useGetOrganizationSettingsQuery } = organizationApi; export const {
useGetTeamQuery,
useGetOrganizationSettingsQuery,
useUpdateOrganizationSettingsMutation,
} = organizationApi;

View File

@ -1,4 +1,4 @@
import { Button, Card, Divider, Flex, Form, Input, Typography } from "antd"; import { Button, Card, Flex, Form, Input } 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 { MyContainer } from "../../shared/components/MyContainer";
@ -6,14 +6,19 @@ 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 { useGetOrganizationSettingsQuery } from "core/services/organization"; import {
useGetOrganizationSettingsQuery,
useUpdateOrganizationSettingsMutation,
} 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 } from "react"; import { useEffect, useRef } from "react";
import { AggregationColor } from "antd/es/color-picker/color"; import { AggregationColor } from "antd/es/color-picker/color";
import { useDispatch } from "react-redux";
import { setPrimaryColor } from "core/reducers/appSlice";
type FieldType = { type FieldType = {
primaryColor: string; primaryColor: string | AggregationColor;
companyName: string; companyName: string;
subdomain: string; subdomain: string;
}; };
@ -27,9 +32,29 @@ export default function Settings() {
); );
const [form] = useForm<FieldType>(); const [form] = useForm<FieldType>();
const dispatch = useDispatch();
const debounceRef = useRef<null | NodeJS.Timeout>(null);
const currentPrimaryColor = useRef<string>();
const [updateOrganizationSettings, { isLoading: isUpdateSettingsLoading }] =
useUpdateOrganizationSettingsMutation();
const handleSave = (values: FieldType) => { const handleSave = (values: FieldType) => {
console.log(values); const hexColor =
typeof values.primaryColor === "string"
? values.primaryColor
: values.primaryColor.toHexString().split("#")[1];
try {
updateOrganizationSettings({
primaryColor: hexColor,
companyName: values.companyName,
});
currentPrimaryColor.current = hexColor;
} catch (error) {
console.error(error);
}
}; };
useEffect(() => { useEffect(() => {
@ -39,9 +64,21 @@ export default function Settings() {
companyName: data.CompanyName, companyName: data.CompanyName,
subdomain: data.Subdomain, subdomain: data.Subdomain,
}); });
currentPrimaryColor.current = data.PrimaryColor;
} }
}, [data]); }, [data]);
useEffect(() => {
return () => {
dispatch(setPrimaryColor(currentPrimaryColor.current));
if (debounceRef.current) {
clearTimeout(debounceRef.current);
}
};
}, []);
if (error) return <MyErrorResult />; if (error) return <MyErrorResult />;
return ( return (
@ -49,127 +86,111 @@ export default function Settings() {
<MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} /> <MyBanner title="Settings" subtitle="MANAGE" headerBar={<HeaderBar />} />
<MyContainer> <MyContainer>
<Flex vertical gap={16} style={{ paddingBottom: 16 }}> <Form form={form} onFinish={handleSave} layout="vertical">
<Form form={form} onFinish={handleSave}> <Card
<Card loading={isLoading}
loading={isLoading} styles={{
styles={{ body: {
body: { padding: 16,
padding: 16, },
}, }}
}} title="Branding"
title="Branding" extra={
extra={ <Button
<Button icon={<SaveOutlined />}
icon={<SaveOutlined />} type="text"
type="text" shape="circle"
shape="circle" size="large"
size="large" htmlType="submit"
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(() => {
dispatch(setPrimaryColor(color.toHexString()));
}, 600);
}}
/> />
} </Form.Item>
> <Form.Item<FieldType> name="companyName" label="Company name">
<Flex vertical gap={2}> <Input defaultValue="Jannex" />
<Flex gap={32}> </Form.Item>
<Flex vertical> <Form.Item<FieldType> name="subdomain" label="Subdomain">
<Typography.Text style={{ fontSize: 16 }}> <Input
Primary Color addonBefore="https://"
</Typography.Text> addonAfter=". jannex.de"
<Form.Item name="primaryColor"> defaultValue="mysite"
<ColorPicker />
defaultValue="#1677ff" </Form.Item>
size="small" <Form.Item label="Logo">
showText <MyUpload
format="hex" action={`/changeCompanyLogo`}
/> onChange={(info) => {
</Form.Item> if (info.file.status === "done") {
</Flex> //onThumbnailChanged?.();
<Flex vertical> console.log("done");
<Typography.Text style={{ fontSize: 16 }}> }
Company name }}
</Typography.Text> imgCropProps={{
<Form.Item name="companyName"> aspect: 1 / 1,
<Input defaultValue="Jannex" /> children: <></>,
</Form.Item> }}
</Flex> >
<Flex vertical> <img
<Typography.Text style={{ fontSize: 16 }}> src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
Subdomain alt="Company Logo"
</Typography.Text> style={{
<Form.Item name="subdomain"> width: 128,
<Input maxHeight: 128,
addonBefore="https://" padding: 4,
addonAfter=". jannex.de" borderRadius: 4,
defaultValue="mysite" border: "1px solid #ddd",
/>
</Form.Item>
</Flex>
<Flex vertical>
<Typography.Text style={{ fontSize: 16 }}>
Logo
</Typography.Text>
<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>
</Flex>
</Flex>
<Flex vertical>
<Typography.Text style={{ fontSize: 16 }}>
Thumbnail
</Typography.Text>
<MyUpload
action={`/changeCompanyLogo`}
onChange={(info) => {
if (info.file.status === "done") {
//onThumbnailChanged?.();
console.log("done");
}
}} }}
imgCropProps={{ />
aspect: 22 / 9, </MyUpload>
children: <></>, </Form.Item>
}} </Flex>
>
<img <Form.Item label="Thumbnail">
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`} <MyUpload
alt="Thumbnail" action={`/changeCompanyLogo`}
style={{ onChange={(info) => {
width: "100%", if (info.file.status === "done") {
height: 228, //onThumbnailChanged?.();
objectFit: "cover", console.log("done");
padding: 4, }
borderRadius: 4, }}
border: "1px solid #ddd", imgCropProps={{
}} aspect: 22 / 9,
/> children: <></>,
</MyUpload> }}
</Flex> >
</Flex> <img
</Card> src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
</Form> alt="Thumbnail"
</Flex> style={{
width: "100%",
height: 228,
objectFit: "cover",
padding: 4,
borderRadius: 4,
border: "1px solid #ddd",
}}
/>
</MyUpload>
</Form.Item>
</Card>
</Form>
</MyContainer> </MyContainer>
</> </>
); );