user profile and AI chat

main
alex 2024-09-09 22:30:05 +02:00
parent bb1bf8ef0e
commit 679ee5bf28
12 changed files with 561 additions and 611 deletions

View File

@ -1,23 +1,13 @@
import { ConfigProvider, Layout, theme } from "antd"; import { ConfigProvider, Layout, theme } from 'antd';
import DashboardLayout from "./core/components/DashboardLayout"; import DashboardLayout from './core/components/DashboardLayout';
import { import { darkMode, primaryColor, setBannerUrl, setLogoUrl, setPrimaryColor, setUserAuthenticated, userAuthenticated, setUserProfilePictureUrl } from './core/reducers/appSlice';
darkMode, import { useDispatch, useSelector } from 'react-redux';
primaryColor, import SignIn from './features/Auth/SignIn';
setBannerUrl, import { useEffect } from 'react';
setLogoUrl, import { myFetch } from './core/utils/utils';
setPrimaryColor, import MyCenteredSpin from './shared/components/MyCenteredSpin';
setUserAuthenticated, import webSocketService, { WebSocketMessageHandler } from 'core/services/websocketService';
userAuthenticated, import { MessageProvider } from 'core/context/MessageContext';
} from "./core/reducers/appSlice";
import { useDispatch, useSelector } from "react-redux";
import SignIn from "./features/Auth/SignIn";
import { useEffect } from "react";
import { myFetch } from "./core/utils/utils";
import MyCenteredSpin from "./shared/components/MyCenteredSpin";
import webSocketService, {
WebSocketMessageHandler,
} from "core/services/websocketService";
import { MessageProvider } from "core/context/MessageContext";
const { defaultAlgorithm, darkAlgorithm } = theme; const { defaultAlgorithm, darkAlgorithm } = theme;
@ -29,10 +19,10 @@ export default function App() {
const primColor = useSelector(primaryColor); 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',
"background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 3px 0 0 3px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)", 'background-color: #555;color: #fff;padding: 3px 2px 3px 3px;border-radius: 3px 0 0 3px;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)',
"background-color: #bc81e0;background-image: linear-gradient(90deg, #e67e22, #9b59b6);color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 3px 3px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)", 'background-color: #bc81e0;background-image: linear-gradient(90deg, #e67e22, #9b59b6);color: #fff;padding: 3px 3px 3px 2px;border-radius: 0 3px 3px 0;font-family: DejaVu Sans,Verdana,Geneva,sans-serif;text-shadow: 0 1px 0 rgba(1, 1, 1, 0.3)',
"background-color: transparent" 'background-color: transparent'
); );
useEffect(() => { useEffect(() => {
@ -40,14 +30,15 @@ export default function App() {
(async () => { (async () => {
try { try {
const response = await myFetch({ const response = await myFetch({
url: "/app", url: '/app',
method: "GET", method: 'GET',
}); });
if (response) { if (response) {
dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`)); dispatch(setPrimaryColor(`#${response.Organization.PrimaryColor}`));
dispatch(setLogoUrl(response.Organization.LogoUrl)); dispatch(setLogoUrl(response.Organization.LogoUrl));
dispatch(setBannerUrl(response.Organization.BannerUrl)); dispatch(setBannerUrl(response.Organization.BannerUrl));
dispatch(setUserProfilePictureUrl(response.User.ProfilePictureUrl));
dispatch(setUserAuthenticated(true)); dispatch(setUserAuthenticated(true));
webSocketService.connect(); webSocketService.connect();
@ -63,7 +54,7 @@ export default function App() {
}, [uAuthenticated]); }, [uAuthenticated]);
useEffect(() => { useEffect(() => {
if (!localStorage.getItem("session")) { if (!localStorage.getItem('session')) {
dispatch(setUserAuthenticated(false)); dispatch(setUserAuthenticated(false));
} else { } else {
dispatch(setUserAuthenticated(true)); dispatch(setUserAuthenticated(true));
@ -71,7 +62,7 @@ export default function App() {
}, [dispatch]); }, [dispatch]);
return ( return (
<Layout style={{ minHeight: "100vh" }}> <Layout style={{ minHeight: '100vh' }}>
<ConfigProvider <ConfigProvider
theme={{ theme={{
algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm, algorithm: isDarkMode ? darkAlgorithm : defaultAlgorithm,
@ -80,15 +71,7 @@ export default function App() {
}, },
}} }}
> >
<MessageProvider> <MessageProvider>{uAuthenticated == null ? <MyCenteredSpin /> : uAuthenticated ? <DashboardLayout /> : <SignIn />}</MessageProvider>
{uAuthenticated == null ? (
<MyCenteredSpin />
) : uAuthenticated ? (
<DashboardLayout />
) : (
<SignIn />
)}
</MessageProvider>
</ConfigProvider> </ConfigProvider>
</Layout> </Layout>
); );

View File

@ -1,42 +1,28 @@
import { Avatar, Dropdown, Flex } from "antd"; import { Avatar, Dropdown, Flex } from 'antd';
import { import { isSideMenuCollapsed, setIsSideMenuCollapsed } from '../SideMenu/sideMenuSlice';
isSideMenuCollapsed, import { useDispatch, useSelector } from 'react-redux';
setIsSideMenuCollapsed, import { EditOutlined, EyeOutlined, LeftOutlined, LogoutOutlined, MenuFoldOutlined, MenuUnfoldOutlined, MoonOutlined, SunOutlined, UserOutlined } from '@ant-design/icons';
} from "../SideMenu/sideMenuSlice"; import { Link, useNavigate } from 'react-router-dom';
import { useDispatch, useSelector } from "react-redux"; import { darkMode, setDarkMode, setUserAuthenticated } from '../../reducers/appSlice';
import { import styles from './styles.module.css';
EditOutlined, import { Constants } from 'core/utils/utils';
EyeOutlined, import webSocketService from 'core/services/websocketService';
LeftOutlined, import { userProfilePictureUrl } from 'core/reducers/appSlice';
LogoutOutlined, import MyUserAvatar from 'shared/components/MyUserAvatar';
MenuFoldOutlined,
MenuUnfoldOutlined,
MoonOutlined,
SunOutlined,
UserOutlined,
} from "@ant-design/icons";
import { Link, useNavigate } from "react-router-dom";
import {
darkMode,
setDarkMode,
setUserAuthenticated,
} from "../../reducers/appSlice";
import styles from "./styles.module.css";
import { Constants } from "core/utils/utils";
import webSocketService from "core/services/websocketService";
type HeaderBarProps = { type HeaderBarProps = {
theme?: "light" | "dark"; theme?: 'light' | 'dark';
onView?: () => void; onView?: () => void;
onEdit?: () => void; onEdit?: () => void;
backTo?: string; backTo?: string;
}; };
export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) { export default function HeaderBar(props: HeaderBarProps = { theme: 'light' }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const isCollpased = useSelector(isSideMenuCollapsed); const isCollpased = useSelector(isSideMenuCollapsed);
const isDarkMode = useSelector(darkMode); const isDarkMode = useSelector(darkMode);
const profilePictureUrl = useSelector(userProfilePictureUrl);
const navigate = useNavigate(); const navigate = useNavigate();
@ -51,26 +37,13 @@ export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
}} }}
> >
<Flex align="center" gap={16}> <Flex align="center" gap={16}>
<div <div className={props.theme === 'light' ? styles.containerLight : styles.containerDark} style={{ borderRadius: 28, padding: 4 }}>
className={
props.theme === "light"
? styles.containerLight
: styles.containerDark
}
style={{ borderRadius: 28, padding: 4 }}
>
{isCollpased ? ( {isCollpased ? (
<div <div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(false))}>
className={styles.iconContainer}
onClick={() => dispatch(setIsSideMenuCollapsed(false))}
>
<MenuUnfoldOutlined className={styles.icon} /> <MenuUnfoldOutlined className={styles.icon} />
</div> </div>
) : ( ) : (
<div <div className={styles.iconContainer} onClick={() => dispatch(setIsSideMenuCollapsed(true))}>
className={styles.iconContainer}
onClick={() => dispatch(setIsSideMenuCollapsed(true))}
>
<MenuFoldOutlined className={styles.icon} /> <MenuFoldOutlined className={styles.icon} />
</div> </div>
)} )}
@ -88,9 +61,7 @@ export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
<Flex <Flex
align="center" align="center"
className={ className={props.theme === 'light' ? styles.containerLight : styles.containerDark}
props.theme === "light" ? styles.containerLight : styles.containerDark
}
style={{ style={{
borderRadius: 28, borderRadius: 28,
paddingLeft: 6, paddingLeft: 6,
@ -113,50 +84,42 @@ export default function HeaderBar(props: HeaderBarProps = { theme: "light" }) {
)} )}
{isDarkMode ? ( {isDarkMode ? (
<div <div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(false))}>
className={styles.iconContainer}
onClick={() => dispatch(setDarkMode(false))}
>
<SunOutlined className={styles.icon} /> <SunOutlined className={styles.icon} />
</div> </div>
) : ( ) : (
<div <div className={styles.iconContainer} onClick={() => dispatch(setDarkMode(true))}>
className={styles.iconContainer}
onClick={() => dispatch(setDarkMode(true))}
>
<MoonOutlined className={styles.icon} /> <MoonOutlined className={styles.icon} />
</div> </div>
)} )}
<Dropdown <Dropdown
overlayStyle={{ minWidth: 150 }} overlayStyle={{ minWidth: 150 }}
trigger={["click"]} trigger={['click']}
menu={{ menu={{
items: [ items: [
{ {
key: "1", key: '1',
label: "Profile", label: 'Profile',
icon: <UserOutlined />, icon: <UserOutlined />,
onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS), onClick: () => navigate(Constants.ROUTE_PATHS.ACCOUNT_SETTINGS),
}, },
{ {
key: "2", key: '2',
label: "Logout", label: 'Logout',
icon: <LogoutOutlined />, icon: <LogoutOutlined />,
danger: true, danger: true,
onClick: () => { onClick: () => {
webSocketService.disconnect(); webSocketService.disconnect();
window.localStorage.removeItem("session"); window.localStorage.removeItem('session');
window.location.href = "/"; window.location.href = '/';
}, },
}, },
], ],
}} }}
> >
<Avatar <div>
size="default" <MyUserAvatar size={34} profilePictureUrl={profilePictureUrl ? profilePictureUrl : ''} />
icon={<UserOutlined />} </div>
style={{ cursor: "pointer" }}
/>
</Dropdown> </Dropdown>
</Flex> </Flex>
</Flex> </Flex>

View File

@ -1,11 +1,12 @@
import { createSlice } from "@reduxjs/toolkit"; import { createSlice } from '@reduxjs/toolkit';
export const appSlice = createSlice({ export const appSlice = createSlice({
name: "app", name: 'app',
initialState: { initialState: {
darkMode: false, darkMode: false,
userAuthenticated: null, userAuthenticated: null,
primaryColor: "#111", userProfilePictureUrl: null,
primaryColor: '#111',
logoUrl: null, logoUrl: null,
bannerUrl: null, bannerUrl: null,
}, },
@ -16,6 +17,9 @@ export const appSlice = createSlice({
setUserAuthenticated: (state, action) => { setUserAuthenticated: (state, action) => {
state.userAuthenticated = action.payload; state.userAuthenticated = action.payload;
}, },
setUserProfilePictureUrl: (state, action) => {
state.userProfilePictureUrl = action.payload;
},
setPrimaryColor: (state, action) => { setPrimaryColor: (state, action) => {
state.primaryColor = action.payload; state.primaryColor = action.payload;
}, },
@ -29,19 +33,13 @@ export const appSlice = createSlice({
selectors: { selectors: {
darkMode: (state) => state.darkMode, darkMode: (state) => state.darkMode,
userAuthenticated: (state) => state.userAuthenticated, userAuthenticated: (state) => state.userAuthenticated,
userProfilePictureUrl: (state) => state.userProfilePictureUrl,
primaryColor: (state) => state.primaryColor, primaryColor: (state) => state.primaryColor,
logoUrl: (state) => state.logoUrl, logoUrl: (state) => state.logoUrl,
bannerUrl: (state) => state.bannerUrl, bannerUrl: (state) => state.bannerUrl,
}, },
}); });
export const { export const { setDarkMode, setUserAuthenticated, setUserProfilePictureUrl, setPrimaryColor, setLogoUrl, setBannerUrl } = appSlice.actions;
setDarkMode,
setUserAuthenticated,
setPrimaryColor,
setLogoUrl,
setBannerUrl,
} = appSlice.actions;
export const { darkMode, userAuthenticated, primaryColor, logoUrl, bannerUrl } = export const { darkMode, userAuthenticated, userProfilePictureUrl, primaryColor, logoUrl, bannerUrl } = appSlice.selectors;
appSlice.selectors;

View File

@ -3,6 +3,7 @@ import { baseQueryWithErrorHandling } from "core/helper/api";
import { import {
Lesson, Lesson,
LessonContent, LessonContent,
LessonQuestion,
LessonSettings, LessonSettings,
UpdateLessonPreviewThumbnail, UpdateLessonPreviewThumbnail,
} from "core/types/lesson"; } from "core/types/lesson";
@ -93,6 +94,12 @@ export const lessonsApi = createApi({
method: "DELETE", method: "DELETE",
}), }),
}), }),
getQuestions: builder.query<LessonQuestion[], string>({
query: (lessonId) => ({
url: `lessons/${lessonId}/questions`,
method: "GET",
}),
}),
}), }),
}); });

View File

@ -1,18 +1,9 @@
import { Dispatch } from "@reduxjs/toolkit"; import { Dispatch } from '@reduxjs/toolkit';
import { import { setBannerUrl, setLogoUrl, setPrimaryColor, setUserProfilePictureUrl } from 'core/reducers/appSlice';
setBannerUrl, import { store } from 'core/store/store';
setLogoUrl, import { BrowserTabSession, Constants } from 'core/utils/utils';
setPrimaryColor, import { WebSocketReceivedMessagesCmds } from 'core/utils/webSocket';
} from "core/reducers/appSlice"; import { addLessonPageContent, deleteLessonPageContent, updateLessonPageContent, updateLessonPageContentPosition } from 'features/Lessons/LessonPage/lessonPageSlice';
import { store } from "core/store/store";
import { BrowserTabSession, Constants } from "core/utils/utils";
import { WebSocketReceivedMessagesCmds } from "core/utils/webSocket";
import {
addLessonPageContent,
deleteLessonPageContent,
updateLessonPageContent,
updateLessonPageContentPosition,
} from "features/Lessons/LessonPage/lessonPageSlice";
import { import {
addLessonContent, addLessonContent,
deleteLessonContent, deleteLessonContent,
@ -21,19 +12,10 @@ import {
setPageEditorLessonState, setPageEditorLessonState,
updateLessonContent, updateLessonContent,
updateLessonContentPosition, updateLessonContentPosition,
} from "features/Lessons/LessonPageEditor/lessonPageEditorSlice"; } from 'features/Lessons/LessonPageEditor/lessonPageEditorSlice';
import { import { addLesson, updateLessonPreviewThumbnail, updateLessonPreviewTitle, updateLessonState } from 'features/Lessons/lessonsSlice';
addLesson, import { addTeamMember, deleteTeamMember, updateTeamMemberRole } from 'features/Team/teamSlice';
updateLessonPreviewThumbnail, import { setProfilePictureUrl } from 'features/UserProfile/userProfileSlice';
updateLessonPreviewTitle,
updateLessonState,
} from "features/Lessons/lessonsSlice";
import {
addTeamMember,
deleteTeamMember,
updateTeamMemberRole,
} from "features/Team/teamSlice";
import { setProfilePictureUrl } from "features/UserProfile/userProfileSlice";
interface WebSocketMessage { interface WebSocketMessage {
Cmd: number; Cmd: number;
@ -47,9 +29,7 @@ class WebSocketService {
private offlineQueue: WebSocketMessage[] = []; private offlineQueue: WebSocketMessage[] = [];
private firstConnect: boolean = true; private firstConnect: boolean = true;
private messageHandler: private messageHandler: ((message: WebSocketMessage, dispatch: Dispatch) => void) | null = null;
| ((message: WebSocketMessage, dispatch: Dispatch) => void)
| null = null;
constructor(url: string) { constructor(url: string) {
this.url = url; this.url = url;
@ -58,14 +38,10 @@ class WebSocketService {
private dispatch: Dispatch | null = null; private dispatch: Dispatch | null = null;
public connect(): void { public connect(): void {
this.socket = new WebSocket( this.socket = new WebSocket(`${this.url}?auth=${localStorage.getItem('session')}&bts=${BrowserTabSession}`);
`${this.url}?auth=${localStorage.getItem(
"session"
)}&bts=${BrowserTabSession}`
);
this.socket.onopen = () => { this.socket.onopen = () => {
console.log("WebSocket connected", this.firstConnect); console.log('WebSocket connected', this.firstConnect);
// Send all messages from the offline queue // Send all messages from the offline queue
@ -87,24 +63,21 @@ class WebSocketService {
if (this.messageHandler) { if (this.messageHandler) {
this.messageHandler(data, this.dispatch!); this.messageHandler(data, this.dispatch!);
} else { } else {
console.error("No handler defined for WebSocket messages"); console.error('No handler defined for WebSocket messages');
} }
}; };
this.socket.onclose = () => { this.socket.onclose = () => {
console.log("WebSocket disconnected. Reconnecting..."); console.log('WebSocket disconnected. Reconnecting...');
setTimeout(() => this.connect(), this.reconnectInterval); setTimeout(() => this.connect(), this.reconnectInterval);
}; };
this.socket.onerror = (error: Event) => { this.socket.onerror = (error: Event) => {
console.error("WebSocket error:", error); console.error('WebSocket error:', error);
}; };
} }
public setHandler( public setHandler(handler: (message: WebSocketMessage, dispatch: Dispatch) => void, dispatch: Dispatch): void {
handler: (message: WebSocketMessage, dispatch: Dispatch) => void,
dispatch: Dispatch
): void {
this.messageHandler = handler; this.messageHandler = handler;
this.dispatch = dispatch; this.dispatch = dispatch;
} }
@ -129,10 +102,10 @@ class WebSocketService {
} }
} }
const webSocketConnectionEventName = "WebSocketConnectionEvent"; const webSocketConnectionEventName = 'WebSocketConnectionEvent';
const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, { const webSocketConnectionEvent = new CustomEvent(webSocketConnectionEventName, {
detail: "wsReconnect", detail: 'wsReconnect',
}); });
export function addWebSocketReconnectListener(callback: () => void): void { export function addWebSocketReconnectListener(callback: () => void): void {
@ -146,13 +119,10 @@ export function removeWebSocketReconnectListener(callback: () => void): void {
const webSocketService = new WebSocketService(Constants.WS_ADDRESS); const webSocketService = new WebSocketService(Constants.WS_ADDRESS);
export default webSocketService; export default webSocketService;
export function WebSocketMessageHandler( export function WebSocketMessageHandler(message: WebSocketMessage, dispatch: Dispatch) {
message: WebSocketMessage,
dispatch: Dispatch
) {
const { Cmd, Body } = message; const { Cmd, Body } = message;
console.log("WebSocketMessageHandler", Cmd, Body); console.log('WebSocketMessageHandler', Cmd, Body);
switch (Cmd) { switch (Cmd) {
case WebSocketReceivedMessagesCmds.SettingsUpdated: case WebSocketReceivedMessagesCmds.SettingsUpdated:
@ -165,11 +135,9 @@ export function WebSocketMessageHandler(
dispatch(setBannerUrl(Body)); dispatch(setBannerUrl(Body));
break; break;
case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain: case WebSocketReceivedMessagesCmds.SettingsUpdatedSubdomain:
localStorage.removeItem("session"); localStorage.removeItem('session');
window.location.href = `${ window.location.href = `${window.location.protocol}//${Body}.${window.location.hostname.split('.').slice(1).join('.')}`;
window.location.protocol
}//${Body}.${window.location.hostname.split(".").slice(1).join(".")}`;
break; break;
case WebSocketReceivedMessagesCmds.TeamAddedMember: case WebSocketReceivedMessagesCmds.TeamAddedMember:
dispatch(addTeamMember(Body)); dispatch(addTeamMember(Body));
@ -279,8 +247,9 @@ export function WebSocketMessageHandler(
break; break;
case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated: case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated:
dispatch(setProfilePictureUrl(Body)); dispatch(setProfilePictureUrl(Body));
dispatch(setUserProfilePictureUrl(Body));
break; break;
default: default:
console.error("Unknown message type:", Cmd); console.error('Unknown message type:', Cmd);
} }
} }

View File

@ -4,6 +4,7 @@ export interface TeamMember {
LastName: string; LastName: string;
Email: string; Email: string;
RoleId: string; RoleId: string;
ProfilePictureUrl: string;
Online: boolean; Online: boolean;
} }

View File

@ -2,6 +2,7 @@ import React from "react";
import { FloatButton } from "antd"; import { FloatButton } from "antd";
import { CommentOutlined } from "@ant-design/icons"; import { CommentOutlined } from "@ant-design/icons";
import { DeepChat } from "deep-chat-react"; import { DeepChat } from "deep-chat-react";
import { getUserSessionFromLocalStorage } from "core/utils/utils";
function AiChat() { function AiChat() {
const [visible, setVisible] = React.useState(false); const [visible, setVisible] = React.useState(false);
@ -30,20 +31,17 @@ function AiChat() {
bottom: 0, bottom: 0,
position: "absolute", position: "absolute",
}} }}
history={[ history={[{ text: "Stell mir Fragen :)", role: "ai" }]}
{ text: "Show me a modern city", role: "user" }, connect={{
{ url: "/api/chat/v1/prompt/",
files: [ method: "POST",
{ headers: {
src: "https://test.ex.umbach.dev/api/statico/809fe37e-8c41-4a44-98d1-d9247affd531/67c763b6-ea67-4b49-9621-2f78b85eb180.png", "X-Authorization": getUserSessionFromLocalStorage() || "",
type: "image",
}, },
], }}
role: "ai", onMessage={async (message) => {
}, console.log("onMessagee", message);
{ text: "Whats on your mind?", role: "user" }, }}
{ text: "Peace and tranquility", role: "ai" },
]}
></DeepChat> ></DeepChat>
</div> </div>
) : null} ) : null}

View File

@ -156,7 +156,7 @@ function GeneralCard({
} }
debounceRef.current = setTimeout(() => { debounceRef.current = setTimeout(() => {
dispatch(setPrimaryColor(color.toHexString())); dispatch(setPrimaryColor(color.toHexString().split("#")[1]));
}, 600); }, 600);
}} }}
/> />
@ -218,12 +218,9 @@ function MediaCard({
<MyUpload <MyUpload
action="/organization/file/banner" action="/organization/file/banner"
onChange={(info) => { onChange={(info) => {
console.log("Banner updated1!", info.file.status);
if (info.file.status === "done" && info.file.response.Data) { if (info.file.status === "done" && info.file.response.Data) {
dispatch(setBannerUrl(info.file.response.Data)); dispatch(setBannerUrl(info.file.response.Data));
console.log("Banner updated!");
success("Banner updated successfully!"); success("Banner updated successfully!");
} }
}} }}
@ -337,14 +334,7 @@ function SubdomainCard({
</p> </p>
</Modal> </Modal>
<Form <Form form={form} layout="vertical" requiredMark={false}>
form={form}
layout="vertical"
requiredMark={false}
onFinish={(values) => {
console.log(values);
}}
>
<MyMiddleCard <MyMiddleCard
title="Subdomain" title="Subdomain"
loading={isLoading} loading={isLoading}

View File

@ -21,6 +21,7 @@ import {
removeWebSocketReconnectListener, removeWebSocketReconnectListener,
} from "core/services/websocketService"; } from "core/services/websocketService";
import { useMessage } from "core/context/MessageContext"; import { useMessage } from "core/context/MessageContext";
import MyUserAvatar from "shared/components/MyUserAvatar";
const TeamList: React.FC = () => { const TeamList: React.FC = () => {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -146,7 +147,17 @@ const TeamList: React.FC = () => {
dataTeamMembers.forEach((item) => { dataTeamMembers.forEach((item) => {
items.push({ items.push({
key: item.Id, key: item.Id,
firstName: item.FirstName, firstName: (
<Space>
<MyUserAvatar
size={42}
profilePictureUrl={item.ProfilePictureUrl}
firstName={item.FirstName}
disableCursorPointer
/>
<span>{item.FirstName}</span>
</Space>
),
lastName: item.LastName, lastName: item.LastName,
email: item.Email, email: item.Email,
role: tmpRoleNames[item.RoleId], role: tmpRoleNames[item.RoleId],

View File

@ -35,6 +35,8 @@ import { tmpRoleNames } from "features/Roles";
import MyErrorResult from "shared/components/MyResult"; import MyErrorResult from "shared/components/MyResult";
import MyUpload from "shared/components/MyUpload"; import MyUpload from "shared/components/MyUpload";
import { useMessage } from "core/context/MessageContext"; import { useMessage } from "core/context/MessageContext";
import MyUserAvatar from "shared/components/MyUserAvatar";
import { setUserProfilePictureUrl } from "core/reducers/appSlice";
export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) { export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -122,6 +124,9 @@ export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
success("Profile picture updated successfully"); success("Profile picture updated successfully");
dispatch(setProfilePictureUrl(info.file.response.Data)); dispatch(setProfilePictureUrl(info.file.response.Data));
dispatch(
setUserProfilePictureUrl(info.file.response.Data)
);
} }
}} }}
imgCropProps={{ imgCropProps={{
@ -129,16 +134,7 @@ export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
children: <></>, children: <></>,
}} }}
> >
{dataProfilePictureUrl === "" ? ( <MyUserAvatar profilePictureUrl={dataProfilePictureUrl} />
<Avatar size={56} style={{ backgroundColor: "#1677ff" }}>
{dataFirstName.charAt(0).toUpperCase()}
</Avatar>
) : (
<Avatar
src={`${Constants.STATIC_CONTENT_ADDRESS}/${dataProfilePictureUrl}`}
size={56}
/>
)}
</MyUpload> </MyUpload>
} }
title={`${dataFirstName} ${dataLastName}`} title={`${dataFirstName} ${dataLastName}`}

View File

@ -0,0 +1,34 @@
import { Avatar } from 'antd';
import { Constants } from 'core/utils/utils';
import { UserOutlined } from '@ant-design/icons';
import { primaryColor } from 'core/reducers/appSlice';
import { useSelector } from 'react-redux';
interface MyUserAvatarProps {
size?: number;
firstName?: string;
profilePictureUrl: string;
disableCursorPointer?: boolean;
}
const MyUserAvatar: React.FC<MyUserAvatarProps> = ({ size = 56, firstName, profilePictureUrl, disableCursorPointer }) => {
const appPrimaryColor = useSelector(primaryColor);
const defaultStyle = disableCursorPointer === undefined ? { cursor: 'pointer' } : {};
const isProfilePictureEmpty = profilePictureUrl === '';
const avatarContent = isProfilePictureEmpty && firstName !== undefined ? firstName.charAt(0) : undefined;
const iconContent = isProfilePictureEmpty && firstName === undefined ? <UserOutlined /> : undefined;
const avatarSrc = isProfilePictureEmpty ? undefined : `${Constants.STATIC_CONTENT_ADDRESS}/${profilePictureUrl}`;
const avatarStyle = isProfilePictureEmpty ? { ...defaultStyle, backgroundColor: `#${appPrimaryColor}` } : defaultStyle;
return (
<div style={{ userSelect: 'none' }}>
<Avatar size={size} style={avatarStyle} src={avatarSrc} icon={iconContent}>
{avatarContent}
</Avatar>
</div>
);
};
export default MyUserAvatar;