mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: adding modal to edit profile
This commit is contained in:
parent
79ca354da1
commit
af69509c61
9 changed files with 219 additions and 7 deletions
|
@ -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",
|
||||
|
|
|
@ -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());
|
||||
|
|
59
src/main/events/profile/update-profile.ts
Normal file
59
src/main/events/profile/update-profile.ts
Normal 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);
|
|
@ -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),
|
||||
|
|
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
|
@ -121,6 +121,10 @@ declare global {
|
|||
|
||||
/* Profile */
|
||||
getMe: () => Promise<UserProfile | null>;
|
||||
updateProfile: (
|
||||
displayName: string,
|
||||
newProfileImagePath: string | null
|
||||
) => Promise<void>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
|
|
|
@ -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>
|
||||
|
|
90
src/renderer/src/pages/user/user-edit-modal.tsx
Normal file
90
src/renderer/src/pages/user/user-edit-modal.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue