update user profile

main
alex 2024-09-08 23:02:23 +02:00
parent 3bba897454
commit bb1bf8ef0e
13 changed files with 350 additions and 192 deletions

View File

@ -12,8 +12,8 @@ import PageNotFound from "../../../features/PageNotFound";
import LessonPage from "../../../features/Lessons/LessonPage"; import LessonPage from "../../../features/Lessons/LessonPage";
import LessonPageEditor from "../../../features/Lessons/LessonPageEditor"; import LessonPageEditor from "../../../features/Lessons/LessonPageEditor";
import TeamCreateUser from "features/Team/CreateUser"; import TeamCreateUser from "features/Team/CreateUser";
import AccountSettings from "features/AccountSettings";
import Board from "features/Board"; import Board from "features/Board";
import UserProfile from "features/UserProfile";
export default function AppRoutes() { export default function AppRoutes() {
return ( return (
@ -94,7 +94,7 @@ export default function AppRoutes() {
path={Constants.ROUTE_PATHS.ACCOUNT_SETTINGS} path={Constants.ROUTE_PATHS.ACCOUNT_SETTINGS}
element={ element={
<MySupsenseFallback> <MySupsenseFallback>
<AccountSettings /> <UserProfile />
</MySupsenseFallback> </MySupsenseFallback>
} }
/> />

View File

@ -0,0 +1,18 @@
import { createApi } from "@reduxjs/toolkit/query/react";
import { baseQueryWithErrorHandling } from "core/helper/api";
import { UserProfile } from "core/types/userProfile";
export const userProfileApi = createApi({
reducerPath: "userProfileApi",
baseQuery: baseQueryWithErrorHandling,
endpoints: (builder) => ({
getUserProfile: builder.query<UserProfile, undefined>({
query: () => ({
url: "user/profile",
method: "GET",
}),
}),
}),
});
export const { useGetUserProfileQuery } = userProfileApi;

View File

@ -33,6 +33,7 @@ import {
deleteTeamMember, deleteTeamMember,
updateTeamMemberRole, updateTeamMemberRole,
} from "features/Team/teamSlice"; } from "features/Team/teamSlice";
import { setProfilePictureUrl } from "features/UserProfile/userProfileSlice";
interface WebSocketMessage { interface WebSocketMessage {
Cmd: number; Cmd: number;
@ -276,6 +277,9 @@ export function WebSocketMessageHandler(
); );
} }
break; break;
case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated:
dispatch(setProfilePictureUrl(Body));
break;
default: default:
console.error("Unknown message type:", Cmd); console.error("Unknown message type:", Cmd);
} }

View File

@ -8,6 +8,8 @@ import { organizationApi } from "core/services/organization";
import { teamSlice } from "features/Team/teamSlice"; import { teamSlice } from "features/Team/teamSlice";
import { lessonsSlice } from "features/Lessons/lessonsSlice"; import { lessonsSlice } from "features/Lessons/lessonsSlice";
import { lessonPageSlice } from "features/Lessons/LessonPage/lessonPageSlice"; import { lessonPageSlice } from "features/Lessons/LessonPage/lessonPageSlice";
import { userProfileApi } from "core/services/userProfile";
import { userProfileSlice } from "features/UserProfile/userProfileSlice";
const makeStore = (/* preloadedState */) => { const makeStore = (/* preloadedState */) => {
const store = configureStore({ const store = configureStore({
@ -21,12 +23,15 @@ const makeStore = (/* preloadedState */) => {
[lessonPageSlice.reducerPath]: lessonPageSlice.reducer, [lessonPageSlice.reducerPath]: lessonPageSlice.reducer,
[organizationApi.reducerPath]: organizationApi.reducer, [organizationApi.reducerPath]: organizationApi.reducer,
[teamSlice.reducerPath]: teamSlice.reducer, [teamSlice.reducerPath]: teamSlice.reducer,
[userProfileApi.reducerPath]: userProfileApi.reducer,
[userProfileSlice.reducerPath]: userProfileSlice.reducer,
}, },
// preloadedState, // preloadedState,
middleware: (getDefaultMiddleware) => middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat( getDefaultMiddleware().concat(
lessonsApi.middleware, lessonsApi.middleware,
organizationApi.middleware organizationApi.middleware,
userProfileApi.middleware
), ),
}); });

View File

@ -0,0 +1,7 @@
export interface UserProfile {
ProfilePictureUrl: string;
FirstName: string;
LastName: string;
Email: string;
RoleId: string;
}

View File

@ -18,7 +18,7 @@ export const Constants = {
ORGANIZATION_TEAM_CREATE_USER: "/team/create-user", ORGANIZATION_TEAM_CREATE_USER: "/team/create-user",
ORGANIZATION_ROLES: "/roles", ORGANIZATION_ROLES: "/roles",
ORGANIZATION_SETTINGS: "/settings", ORGANIZATION_SETTINGS: "/settings",
ACCOUNT_SETTINGS: "/account", ACCOUNT_SETTINGS: "/user-profile",
WHATS_NEW: "/whats-new", WHATS_NEW: "/whats-new",
SUGGEST_FEATURE: "/suggest-feature", SUGGEST_FEATURE: "/suggest-feature",
CONTACT_SUPPORT: "/contact-support", CONTACT_SUPPORT: "/contact-support",

View File

@ -19,6 +19,7 @@ enum WebSocketReceivedMessagesCmds {
LessonContentUpdated = 14, LessonContentUpdated = 14,
LessonContentUpdatedPosition = 15, LessonContentUpdatedPosition = 15,
LessonContentFileUpdated = 16, LessonContentFileUpdated = 16,
UserProfilePictureUpdated = 17,
} }
export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds }; export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds };

View File

@ -1,164 +0,0 @@
import {
Avatar,
Button,
Card,
Descriptions,
Divider,
Flex,
Form,
Input,
Typography,
} 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 MyMiddleCard from "shared/components/MyMiddleCard";
import Meta from "antd/es/card/Meta";
export default function AccountSettings({ isAdmin }: { isAdmin?: boolean }) {
function AdminWrapper({ children }: { children: React.ReactNode }) {
if (!isAdmin) {
return <>{children}</>;
}
return (
<Form layout="vertical" style={{ marginTop: 24 }}>
{children}
</Form>
);
}
function TextItem({ value, name }: { value: string; name: string }) {
if (!isAdmin) {
return <>{value}</>;
}
return (
<Form.Item name={name} style={{ width: "100%" }} required>
<Input defaultValue={value} />
</Form.Item>
);
}
return (
<>
<MyBanner
title="Account Settings"
subtitle="MANAGE"
headerBar={<HeaderBar />}
/>
<MyMiddleCard title="My Profile">
<Flex vertical gap={16} style={{ paddingBottom: 16 }}>
<Card
styles={{
body: {
padding: 16,
},
}}
>
<Meta
avatar={
<Avatar
src={`${Constants.STATIC_CONTENT_ADDRESS}/demo/lesson_thumbnail.webp`}
size={56}
/>
}
title="Jorg Kreith"
description="Lead"
/>
</Card>
{/*<Card
styles={{
body: {
padding: 16,
},
}}
>
<Meta title="Personal Information" />
<Flex gap={16} style={{ marginTop: 24 }}>
<Flex vertical gap={24} flex={1}>
<Flex gap={8} vertical>
<TitleText>First name</TitleText>
<ValueText>Jorg</ValueText>
</Flex>
<Flex gap={8} vertical>
<TitleText>Email</TitleText>
<ValueText>julian@xx.com</ValueText>
</Flex>
</Flex>
<Flex vertical gap={24} flex={1}>
<Flex gap={8} vertical>
<TitleText>Last name</TitleText>
<ValueText>Kreth</ValueText>
</Flex>
</Flex>
</Flex>
</Card>*/}
<Card
styles={{
body: {
padding: 16,
},
}}
>
<AdminWrapper>
<Descriptions
title="Personal Information"
layout="vertical"
items={[
{
key: "1",
label: "First name",
children: <TextItem value="Jorg" name="firstName" />,
},
{
key: "2",
label: "Last name",
children: <TextItem value="Kreth" name="lastName" />,
},
{
key: "3",
label: "Email",
children: <TextItem value="julian@xx.com" name="email" />,
},
]}
/>
</AdminWrapper>
</Card>
</Flex>
</MyMiddleCard>
</>
);
}
/*
// TODO: sessions table
<Card
styles={{
body: {
padding: 16,
},
}}
>
<Meta title="Sessions" />
<Typography.Text>// Todoo</Typography.Text>
</Card>
*/
function TitleText({ children }: { children: React.ReactNode }) {
return (
<Typography.Text style={{ fontSize: 16, opacity: 0.4 }}>
{children}
</Typography.Text>
);
}
function ValueText({ children }: { children: React.ReactNode }) {
return <Typography.Text style={{ fontSize: 16 }}>{children}</Typography.Text>;
}

View File

@ -1,7 +1,21 @@
import { Button, Result } from "antd";
import { Constants } from "core/utils/utils";
import { Link } from "react-router-dom";
import { MyCenteredContainer } from "shared/components/MyContainer";
export default function PageNotFound() { export default function PageNotFound() {
return ( return (
<> <MyCenteredContainer>
<h1>PageNotFound</h1> <Result
</> status="404"
title="404"
subTitle="Sorry, the page you visited does not exist."
extra={
<Link to={Constants.ROUTE_PATHS.BOARD}>
<Button type="primary">Back Home</Button>
</Link>
}
/>
</MyCenteredContainer>
); );
} }

View File

@ -199,7 +199,7 @@ function MediaCard({
}} }}
> >
<img <img
src={`${Constants.STATIC_CONTENT_ADDRESS}${ src={`${Constants.STATIC_CONTENT_ADDRESS}/${
appLogoUrl || Constants.DEMO_LOGO_URL appLogoUrl || Constants.DEMO_LOGO_URL
}`} }`}
alt="Company Logo" alt="Company Logo"
@ -233,7 +233,7 @@ function MediaCard({
}} }}
> >
<img <img
src={`${Constants.STATIC_CONTENT_ADDRESS}${ src={`${Constants.STATIC_CONTENT_ADDRESS}/${
appBannerUrl || Constants.DEMO_BANNER_URL appBannerUrl || Constants.DEMO_BANNER_URL
}`} }`}
alt="Banner" alt="Banner"

View File

@ -0,0 +1,216 @@
import {
Avatar,
Card,
Descriptions,
Flex,
Form,
Input,
Typography,
} from "antd";
import HeaderBar from "../../core/components/Header";
import MyBanner from "../../shared/components/MyBanner";
import { Constants } from "core/utils/utils";
import MyMiddleCard from "shared/components/MyMiddleCard";
import Meta from "antd/es/card/Meta";
import { useGetUserProfileQuery } from "core/services/userProfile";
import { useEffect } from "react";
import {
addWebSocketReconnectListener,
removeWebSocketReconnectListener,
} from "core/services/websocketService";
import { useDispatch, useSelector } from "react-redux";
import {
email,
firstName,
lastName,
profilePictureUrl,
roleId,
setEmail,
setFirstName,
setLastName,
setProfilePictureUrl,
setRoleId,
} from "./userProfileSlice";
import { tmpRoleNames } from "features/Roles";
import MyErrorResult from "shared/components/MyResult";
import MyUpload from "shared/components/MyUpload";
import { useMessage } from "core/context/MessageContext";
export default function UserProfile({ isAdmin }: { isAdmin?: boolean }) {
const dispatch = useDispatch();
const { success } = useMessage();
const dataProfilePictureUrl = useSelector(profilePictureUrl);
const dataFirstName = useSelector(firstName);
const dataLastName = useSelector(lastName);
const dataEmail = useSelector(email);
const dataRoleId = useSelector(roleId);
const { data, error, isLoading, refetch } = useGetUserProfileQuery(
undefined,
{
refetchOnMountOrArgChange: true,
}
);
function AdminWrapper({ children }: { children: React.ReactNode }) {
if (!isAdmin) {
return <>{children}</>;
}
return (
<Form layout="vertical" style={{ marginTop: 24 }}>
{children}
</Form>
);
}
function TextItem({ value, name }: { value: string; name: string }) {
if (!isAdmin) {
return <>{value}</>;
}
return (
<Form.Item name={name} style={{ width: "100%" }} required>
<Input defaultValue={value} />
</Form.Item>
);
}
useEffect(() => {
if (!data) return;
dispatch(setProfilePictureUrl(data.ProfilePictureUrl));
dispatch(setFirstName(data.FirstName));
dispatch(setLastName(data.LastName));
dispatch(setEmail(data.Email));
dispatch(setRoleId(data.RoleId));
}, [data]);
useEffect(() => {
addWebSocketReconnectListener(refetch);
return () => removeWebSocketReconnectListener(refetch);
}, []);
return (
<>
<MyBanner
title="Account Settings"
subtitle="MANAGE"
headerBar={<HeaderBar />}
/>
<MyMiddleCard title="My Profile" loading={isLoading}>
{error ? (
<MyErrorResult />
) : (
<Flex vertical gap={16} style={{ paddingBottom: 16 }}>
<Card
styles={{
body: {
padding: 16,
},
}}
>
<Meta
avatar={
<MyUpload
action={`/user/profile/picture`}
onChange={(info) => {
if (info.file.status === "done") {
success("Profile picture updated successfully");
dispatch(setProfilePictureUrl(info.file.response.Data));
}
}}
imgCropProps={{
aspect: 1 / 1,
children: <></>,
}}
>
{dataProfilePictureUrl === "" ? (
<Avatar size={56} style={{ backgroundColor: "#1677ff" }}>
{dataFirstName.charAt(0).toUpperCase()}
</Avatar>
) : (
<Avatar
src={`${Constants.STATIC_CONTENT_ADDRESS}/${dataProfilePictureUrl}`}
size={56}
/>
)}
</MyUpload>
}
title={`${dataFirstName} ${dataLastName}`}
description={tmpRoleNames[dataRoleId]}
/>
</Card>
<Card
styles={{
body: {
padding: 16,
},
}}
>
<AdminWrapper>
<Descriptions
title="Personal Information"
layout="vertical"
items={[
{
key: "1",
label: "First name",
children: (
<TextItem value={dataFirstName} name="firstName" />
),
},
{
key: "2",
label: "Last name",
children: (
<TextItem value={dataLastName} name="lastName" />
),
},
{
key: "3",
label: "Email",
children: <TextItem value={dataEmail} name="email" />,
},
]}
/>
</AdminWrapper>
</Card>
</Flex>
)}
</MyMiddleCard>
</>
);
}
/*
// TODO: sessions table
<Card
styles={{
body: {
padding: 16,
},
}}
>
<Meta title="Sessions" />
<Typography.Text>// Todoo</Typography.Text>
</Card>
*/
function TitleText({ children }: { children: React.ReactNode }) {
return (
<Typography.Text style={{ fontSize: 16, opacity: 0.4 }}>
{children}
</Typography.Text>
);
}
function ValueText({ children }: { children: React.ReactNode }) {
return <Typography.Text style={{ fontSize: 16 }}>{children}</Typography.Text>;
}

View File

@ -0,0 +1,47 @@
import { createSlice } from "@reduxjs/toolkit";
export const userProfileSlice = createSlice({
name: "userProfile",
initialState: {
profilePictureUrl: "",
firstName: "",
lastName: "",
email: "",
roleId: "",
},
reducers: {
setProfilePictureUrl: (state, action) => {
state.profilePictureUrl = action.payload;
},
setFirstName: (state, action) => {
state.firstName = action.payload;
},
setLastName: (state, action) => {
state.lastName = action.payload;
},
setEmail: (state, action) => {
state.email = action.payload;
},
setRoleId: (state, action) => {
state.roleId = action.payload;
},
},
selectors: {
profilePictureUrl: (state) => state.profilePictureUrl,
firstName: (state) => state.firstName,
lastName: (state) => state.lastName,
email: (state) => state.email,
roleId: (state) => state.roleId,
},
});
export const {
setEmail,
setFirstName,
setLastName,
setProfilePictureUrl,
setRoleId,
} = userProfileSlice.actions;
export const { profilePictureUrl, firstName, lastName, email, roleId } =
userProfileSlice.selectors;

View File

@ -42,29 +42,39 @@ export default function MyUpload({
? accept.join(",") ? accept.join(",")
: (accept as string); : (accept as string);
const MyUpload = () => (
<Upload
accept={acceptFileTypes}
maxCount={maxCount}
showUploadList={showUploadList}
headers={headers}
action={`${Constants.API_ADDRESS}${action}`}
onChange={(info) => {
if (onChange) onChange(info);
}}
beforeUpload={beforeUpload}
>
{children}
</Upload>
);
if (fileType === "video") { if (fileType === "video") {
return <MyUpload />; return (
<Upload
accept={acceptFileTypes}
maxCount={maxCount}
showUploadList={showUploadList}
headers={headers}
action={`${Constants.API_ADDRESS}${action}`}
onChange={(info) => {
if (onChange) onChange(info);
}}
beforeUpload={beforeUpload}
>
{children}
</Upload>
);
} }
return ( return (
<ImgCrop {...imgCropProps} rotationSlider> <ImgCrop {...imgCropProps} rotationSlider>
<MyUpload /> <Upload
accept={acceptFileTypes}
maxCount={maxCount}
showUploadList={showUploadList}
headers={headers}
action={`${Constants.API_ADDRESS}${action}`}
onChange={(info) => {
if (onChange) onChange(info);
}}
beforeUpload={beforeUpload}
>
{children}
</Upload>
</ImgCrop> </ImgCrop>
); );
} }