feat: adding modal to edit profile

This commit is contained in:
Zamitto 2024-06-17 21:55:43 -03:00
parent 79ca354da1
commit af69509c61
9 changed files with 219 additions and 7 deletions

View file

@ -61,6 +61,7 @@
"jsdom": "^24.0.0",
"lodash-es": "^4.17.21",
"lottie-react": "^2.4.0",
"mime": "^4.0.3",
"parse-torrent": "^11.0.16",
"piscina": "^4.5.1",
"ps-list": "^8.1.1",

View file

@ -42,6 +42,7 @@ import "./download-sources/sync-download-sources";
import "./auth/signout";
import "./user/get-user";
import "./profile/get-me";
import "./profile/update-profile";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => app.getVersion());

View file

@ -0,0 +1,59 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services/hydra-api";
import axios from "axios";
import fs from "node:fs";
import mime from "mime";
const patchUserProfile = (displayName: string, imageUrl?: string) => {
return;
if (imageUrl) {
return HydraApi.patch("/profile", {
displayName,
imageUrl,
});
} else {
return HydraApi.patch("/profile", {
displayName,
});
}
};
const updateProfile = async (
_event: Electron.IpcMainInvokeEvent,
displayName: string,
newProfileImagePath: string | null
): Promise<any> => {
if (!newProfileImagePath) {
patchUserProfile(displayName);
return;
}
const stats = fs.statSync(newProfileImagePath);
const fileBuffer = fs.readFileSync(newProfileImagePath);
const fileSizeInBytes = stats.size;
const profileImageUrl = await HydraApi.post(`/presigned-urls/profile-image`, {
imageExt: newProfileImagePath.split(".").at(-1),
imageLength: fileSizeInBytes,
})
.then(async (preSignedResponse) => {
const { presignedUrl, profileImageUrl } = preSignedResponse.data;
const mimeType = mime.getType(newProfileImagePath);
await axios.put(presignedUrl, fileBuffer, {
headers: {
"Content-Type": mimeType,
},
});
return profileImageUrl;
})
.catch(() => {
return undefined;
});
console.log(profileImageUrl);
patchUserProfile(displayName, profileImageUrl);
};
registerEvent("updateProfile", updateProfile);

View file

@ -128,6 +128,8 @@ contextBridge.exposeInMainWorld("electron", {
/* Profile */
getMe: () => ipcRenderer.invoke("getMe"),
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
/* User */
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),

View file

@ -121,6 +121,10 @@ declare global {
/* Profile */
getMe: () => Promise<UserProfile | null>;
updateProfile: (
displayName: string,
newProfileImagePath: string | null
) => Promise<void>;
}
interface Window {

View file

@ -3,7 +3,7 @@ import cn from "classnames";
import * as styles from "./user.css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { useMemo } from "react";
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { useDate, useUserDetails } from "@renderer/hooks";
@ -11,6 +11,7 @@ import { useNavigate } from "react-router-dom";
import { buildGameDetailsPath } from "@renderer/helpers";
import { PersonIcon } from "@primer/octicons-react";
import { Button } from "@renderer/components";
import { UserEditProfileModal } from "./user-edit-modal";
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
@ -23,6 +24,8 @@ export function UserContent({ userProfile }: ProfileContentProps) {
const { userDetails, profileBackground, signOut } = useUserDetails();
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
const navigate = useNavigate();
const numberFormatter = useMemo(() => {
@ -54,6 +57,10 @@ export function UserContent({ userProfile }: ProfileContentProps) {
navigate(buildGameDetailsPath(game));
};
const handleEditProfile = () => {
setShowEditProfileModal(true);
};
const handleSignout = async () => {
await signOut();
navigate("/");
@ -69,10 +76,17 @@ export function UserContent({ userProfile }: ProfileContentProps) {
return (
<>
<UserEditProfileModal
visible={showEditProfileModal}
onClose={() => setShowEditProfileModal(false)}
userProfile={userProfile}
/>
<section
className={styles.profileContentBox}
style={{
background: profileContentBoxBackground,
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 2}px`,
}}
>
<div className={styles.profileAvatarContainer}>
@ -93,9 +107,23 @@ export function UserContent({ userProfile }: ProfileContentProps) {
{isMe && (
<div style={{ flex: 1, display: "flex", justifyContent: "end" }}>
<Button theme="danger" onClick={handleSignout}>
{t("sign_out")}
</Button>
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
}}
>
<>
<Button theme="outline" onClick={handleEditProfile}>
Editar perfil
</Button>
<Button theme="danger" onClick={handleSignout}>
{t("sign_out")}
</Button>
</>
</div>
</div>
)}
</section>

View file

@ -0,0 +1,90 @@
import { Button, Modal, TextField } from "@renderer/components";
import { UserProfile } from "@types";
import * as styles from "./user.css";
import { PersonIcon } from "@primer/octicons-react";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useState } from "react";
export interface UserEditProfileModalProps {
userProfile: UserProfile;
visible: boolean;
onClose: () => void;
}
export const UserEditProfileModal = ({
userProfile,
visible,
onClose,
}: UserEditProfileModalProps) => {
const [displayName, setDisplayName] = useState(userProfile.displayName);
const [newImagePath, setNewImagePath] = useState<string | null>(null);
const handleChangeProfileAvatar = async () => {
const { filePaths } = await window.electron.showOpenDialog({
properties: ["openFile"],
filters: [
{
name: "Profile avatar",
extensions: ["jpg", "png", "gif"],
},
],
});
const path = filePaths[0];
console.log(path);
setNewImagePath(path);
};
const handleSaveProfile = async () => {
await window.electron
.updateProfile(displayName, newImagePath)
.catch((err) => {
console.log("errro", err);
});
setNewImagePath(null);
onClose();
};
return (
<>
<Modal visible={visible} title="Editar Perfil" onClose={onClose}>
<section
style={{
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
gap: `${SPACING_UNIT * 3}px`,
width: "300px",
}}
>
<button
className={styles.profileAvatarEditContainer}
onClick={handleChangeProfileAvatar}
>
{userProfile.profileImageUrl ? (
<img
className={styles.profileAvatar}
alt={userProfile.displayName}
src={newImagePath ?? userProfile.profileImageUrl}
/>
) : (
<PersonIcon size={72} />
)}
</button>
<TextField
label="Nome de exibição"
value={displayName}
containerProps={{ style: { width: "100%" } }}
onChange={(e) => setDisplayName(e.target.value)}
/>
<Button style={{ alignSelf: "end" }} onClick={handleSaveProfile}>
Salvar{" "}
</Button>
</section>
</Modal>
</>
);
};

View file

@ -12,7 +12,6 @@ export const wrapper = style({
export const profileContentBox = style({
display: "flex",
gap: `${SPACING_UNIT * 3}px`,
padding: `${SPACING_UNIT * 4}px ${SPACING_UNIT * 2}px`,
alignItems: "center",
borderRadius: "4px",
border: `solid 1px ${vars.color.border}`,
@ -36,12 +35,35 @@ export const profileAvatarContainer = style({
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
});
export const profileAvatarEditContainer = style({
width: "128px",
height: "128px",
borderRadius: "50%",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: vars.color.background,
position: "relative",
overflow: "hidden",
border: `solid 1px ${vars.color.border}`,
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
});
export const profileAvatar = style({
width: "96px",
height: "96px",
height: "100%",
objectFit: "cover",
});
export const profileAvatarEditOverlay = style({
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "#00000055",
color: vars.color.muted,
zIndex: 1,
cursor: "pointer",
});
export const profileInformation = style({
display: "flex",
flexDirection: "column",

View file

@ -4518,6 +4518,11 @@ mime@^2.5.2:
resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367"
integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==
mime@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/mime/-/mime-4.0.3.tgz#cd4a1aa052fc980dfc34f111fe1be9e8b878c5d2"
integrity sha512-KgUb15Oorc0NEKPbvfa0wRU+PItIEZmiv+pyAO2i0oTIVTJhlzMclU7w4RXWQrSOVH5ax/p/CkIO7KI4OyFJTQ==
mimic-fn@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc"