update user profile
parent
3bba897454
commit
bb1bf8ef0e
|
@ -12,8 +12,8 @@ import PageNotFound from "../../../features/PageNotFound";
|
|||
import LessonPage from "../../../features/Lessons/LessonPage";
|
||||
import LessonPageEditor from "../../../features/Lessons/LessonPageEditor";
|
||||
import TeamCreateUser from "features/Team/CreateUser";
|
||||
import AccountSettings from "features/AccountSettings";
|
||||
import Board from "features/Board";
|
||||
import UserProfile from "features/UserProfile";
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
|
@ -94,7 +94,7 @@ export default function AppRoutes() {
|
|||
path={Constants.ROUTE_PATHS.ACCOUNT_SETTINGS}
|
||||
element={
|
||||
<MySupsenseFallback>
|
||||
<AccountSettings />
|
||||
<UserProfile />
|
||||
</MySupsenseFallback>
|
||||
}
|
||||
/>
|
||||
|
|
|
@ -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;
|
|
@ -33,6 +33,7 @@ import {
|
|||
deleteTeamMember,
|
||||
updateTeamMemberRole,
|
||||
} from "features/Team/teamSlice";
|
||||
import { setProfilePictureUrl } from "features/UserProfile/userProfileSlice";
|
||||
|
||||
interface WebSocketMessage {
|
||||
Cmd: number;
|
||||
|
@ -276,6 +277,9 @@ export function WebSocketMessageHandler(
|
|||
);
|
||||
}
|
||||
break;
|
||||
case WebSocketReceivedMessagesCmds.UserProfilePictureUpdated:
|
||||
dispatch(setProfilePictureUrl(Body));
|
||||
break;
|
||||
default:
|
||||
console.error("Unknown message type:", Cmd);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import { organizationApi } from "core/services/organization";
|
|||
import { teamSlice } from "features/Team/teamSlice";
|
||||
import { lessonsSlice } from "features/Lessons/lessonsSlice";
|
||||
import { lessonPageSlice } from "features/Lessons/LessonPage/lessonPageSlice";
|
||||
import { userProfileApi } from "core/services/userProfile";
|
||||
import { userProfileSlice } from "features/UserProfile/userProfileSlice";
|
||||
|
||||
const makeStore = (/* preloadedState */) => {
|
||||
const store = configureStore({
|
||||
|
@ -21,12 +23,15 @@ const makeStore = (/* preloadedState */) => {
|
|||
[lessonPageSlice.reducerPath]: lessonPageSlice.reducer,
|
||||
[organizationApi.reducerPath]: organizationApi.reducer,
|
||||
[teamSlice.reducerPath]: teamSlice.reducer,
|
||||
[userProfileApi.reducerPath]: userProfileApi.reducer,
|
||||
[userProfileSlice.reducerPath]: userProfileSlice.reducer,
|
||||
},
|
||||
// preloadedState,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(
|
||||
lessonsApi.middleware,
|
||||
organizationApi.middleware
|
||||
organizationApi.middleware,
|
||||
userProfileApi.middleware
|
||||
),
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export interface UserProfile {
|
||||
ProfilePictureUrl: string;
|
||||
FirstName: string;
|
||||
LastName: string;
|
||||
Email: string;
|
||||
RoleId: string;
|
||||
}
|
|
@ -18,7 +18,7 @@ export const Constants = {
|
|||
ORGANIZATION_TEAM_CREATE_USER: "/team/create-user",
|
||||
ORGANIZATION_ROLES: "/roles",
|
||||
ORGANIZATION_SETTINGS: "/settings",
|
||||
ACCOUNT_SETTINGS: "/account",
|
||||
ACCOUNT_SETTINGS: "/user-profile",
|
||||
WHATS_NEW: "/whats-new",
|
||||
SUGGEST_FEATURE: "/suggest-feature",
|
||||
CONTACT_SUPPORT: "/contact-support",
|
||||
|
|
|
@ -19,6 +19,7 @@ enum WebSocketReceivedMessagesCmds {
|
|||
LessonContentUpdated = 14,
|
||||
LessonContentUpdatedPosition = 15,
|
||||
LessonContentFileUpdated = 16,
|
||||
UserProfilePictureUpdated = 17,
|
||||
}
|
||||
|
||||
export { WebSocketSendMessagesCmds, WebSocketReceivedMessagesCmds };
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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() {
|
||||
return (
|
||||
<>
|
||||
<h1>PageNotFound</h1>
|
||||
</>
|
||||
<MyCenteredContainer>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -199,7 +199,7 @@ function MediaCard({
|
|||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/${
|
||||
appLogoUrl || Constants.DEMO_LOGO_URL
|
||||
}`}
|
||||
alt="Company Logo"
|
||||
|
@ -233,7 +233,7 @@ function MediaCard({
|
|||
}}
|
||||
>
|
||||
<img
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}${
|
||||
src={`${Constants.STATIC_CONTENT_ADDRESS}/${
|
||||
appBannerUrl || Constants.DEMO_BANNER_URL
|
||||
}`}
|
||||
alt="Banner"
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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;
|
|
@ -42,29 +42,39 @@ export default function MyUpload({
|
|||
? 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) onChange(info);
|
||||
}}
|
||||
beforeUpload={beforeUpload}
|
||||
>
|
||||
{children}
|
||||
</Upload>
|
||||
);
|
||||
|
||||
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 (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue