mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-01-23 21:44:55 +03:00
Merge pull request #814 from hydralauncher/hyd-270-create-a-section-under-library-games-in-profile-page-for
feat: add friends
This commit is contained in:
commit
3952f106fc
@ -40,6 +40,7 @@
|
|||||||
"@reduxjs/toolkit": "^2.2.3",
|
"@reduxjs/toolkit": "^2.2.3",
|
||||||
"@sentry/electron": "^5.1.0",
|
"@sentry/electron": "^5.1.0",
|
||||||
"@vanilla-extract/css": "^1.14.2",
|
"@vanilla-extract/css": "^1.14.2",
|
||||||
|
"@vanilla-extract/dynamic": "^2.1.1",
|
||||||
"@vanilla-extract/recipes": "^0.5.2",
|
"@vanilla-extract/recipes": "^0.5.2",
|
||||||
"aria2": "^4.1.2",
|
"aria2": "^4.1.2",
|
||||||
"auto-launch": "^5.0.6",
|
"auto-launch": "^5.0.6",
|
||||||
|
@ -241,6 +241,15 @@
|
|||||||
"successfully_signed_out": "Successfully signed out",
|
"successfully_signed_out": "Successfully signed out",
|
||||||
"sign_out": "Sign out",
|
"sign_out": "Sign out",
|
||||||
"playing_for": "Playing for {{amount}}",
|
"playing_for": "Playing for {{amount}}",
|
||||||
"sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out?"
|
"sign_out_modal_text": "Your library is linked with your current account. When signing out, your library will not be visible anymore, and any progress will not be saved. Continue with sign out?",
|
||||||
|
"add_friends": "Add Friends",
|
||||||
|
"add": "Add",
|
||||||
|
"friend_code": "Friend code",
|
||||||
|
"see_profile": "See profile",
|
||||||
|
"sending": "Sending",
|
||||||
|
"friend_request_sent": "Friend request sent",
|
||||||
|
"friends": "Friends",
|
||||||
|
"friends_list": "Friends list",
|
||||||
|
"user_not_found": "User not found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,6 +241,15 @@
|
|||||||
"sign_out": "Sair da conta",
|
"sign_out": "Sair da conta",
|
||||||
"sign_out_modal_title": "Tem certeza?",
|
"sign_out_modal_title": "Tem certeza?",
|
||||||
"playing_for": "Jogando por {{amount}}",
|
"playing_for": "Jogando por {{amount}}",
|
||||||
"sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?"
|
"sign_out_modal_text": "Sua biblioteca de jogos está associada com a sua conta atual. Ao sair, sua biblioteca não aparecerá mais no Hydra e qualquer progresso não será salvo. Deseja continuar?",
|
||||||
|
"add_friends": "Adicionar Amigos",
|
||||||
|
"friend_code": "Código de amigo",
|
||||||
|
"see_profile": "Ver perfil",
|
||||||
|
"friend_request_sent": "Pedido de amizade enviado",
|
||||||
|
"friends": "Amigos",
|
||||||
|
"add": "Adicionar",
|
||||||
|
"sending": "Enviando",
|
||||||
|
"friends_list": "Lista de amigos",
|
||||||
|
"user_not_found": "Usuário não encontrado"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,11 @@ import "./auth/sign-out";
|
|||||||
import "./auth/open-auth-window";
|
import "./auth/open-auth-window";
|
||||||
import "./auth/get-session-hash";
|
import "./auth/get-session-hash";
|
||||||
import "./user/get-user";
|
import "./user/get-user";
|
||||||
|
import "./profile/get-friend-requests";
|
||||||
import "./profile/get-me";
|
import "./profile/get-me";
|
||||||
|
import "./profile/update-friend-request";
|
||||||
import "./profile/update-profile";
|
import "./profile/update-profile";
|
||||||
|
import "./profile/send-friend-request";
|
||||||
|
|
||||||
ipcMain.handle("ping", () => "pong");
|
ipcMain.handle("ping", () => "pong");
|
||||||
ipcMain.handle("getVersion", () => app.getVersion());
|
ipcMain.handle("getVersion", () => app.getVersion());
|
||||||
|
11
src/main/events/profile/get-friend-requests.ts
Normal file
11
src/main/events/profile/get-friend-requests.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import { FriendRequest } from "@types";
|
||||||
|
|
||||||
|
const getFriendRequests = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent
|
||||||
|
): Promise<FriendRequest[]> => {
|
||||||
|
return HydraApi.get(`/profile/friend-requests`).catch(() => []);
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("getFriendRequests", getFriendRequests);
|
@ -9,9 +9,7 @@ const getMe = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent
|
_event: Electron.IpcMainInvokeEvent
|
||||||
): Promise<UserProfile | null> => {
|
): Promise<UserProfile | null> => {
|
||||||
return HydraApi.get(`/profile/me`)
|
return HydraApi.get(`/profile/me`)
|
||||||
.then((response) => {
|
.then((me) => {
|
||||||
const me = response.data;
|
|
||||||
|
|
||||||
userAuthRepository.upsert(
|
userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -26,12 +24,18 @@ const getMe = async (
|
|||||||
|
|
||||||
return me;
|
return me;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(async (err) => {
|
||||||
if (err instanceof UserNotLoggedInError) {
|
if (err instanceof UserNotLoggedInError) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return userAuthRepository.findOne({ where: { id: 1 } });
|
const loggedUser = await userAuthRepository.findOne({ where: { id: 1 } });
|
||||||
|
|
||||||
|
if (loggedUser) {
|
||||||
|
return { ...loggedUser, id: loggedUser.userId };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
11
src/main/events/profile/send-friend-request.ts
Normal file
11
src/main/events/profile/send-friend-request.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
|
||||||
|
const sendFriendRequest = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
userId: string
|
||||||
|
) => {
|
||||||
|
return HydraApi.post("/profile/friend-requests", { friendCode: userId });
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("sendFriendRequest", sendFriendRequest);
|
19
src/main/events/profile/update-friend-request.ts
Normal file
19
src/main/events/profile/update-friend-request.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { registerEvent } from "../register-event";
|
||||||
|
import { HydraApi } from "@main/services";
|
||||||
|
import { FriendRequestAction } from "@types";
|
||||||
|
|
||||||
|
const updateFriendRequest = async (
|
||||||
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
|
userId: string,
|
||||||
|
action: FriendRequestAction
|
||||||
|
) => {
|
||||||
|
if (action == "CANCEL") {
|
||||||
|
return HydraApi.delete(`/profile/friend-requests/${userId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HydraApi.patch(`/profile/friend-requests/${userId}`, {
|
||||||
|
requestState: action,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
registerEvent("updateFriendRequest", updateFriendRequest);
|
@ -26,11 +26,9 @@ const updateProfile = async (
|
|||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
newProfileImagePath: string | null
|
newProfileImagePath: string | null
|
||||||
) => {
|
): Promise<UserProfile> => {
|
||||||
if (!newProfileImagePath) {
|
if (!newProfileImagePath) {
|
||||||
return patchUserProfile(displayName).then(
|
return patchUserProfile(displayName);
|
||||||
(response) => response.data as UserProfile
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stats = fs.statSync(newProfileImagePath);
|
const stats = fs.statSync(newProfileImagePath);
|
||||||
@ -42,7 +40,7 @@ const updateProfile = async (
|
|||||||
imageLength: fileSizeInBytes,
|
imageLength: fileSizeInBytes,
|
||||||
})
|
})
|
||||||
.then(async (preSignedResponse) => {
|
.then(async (preSignedResponse) => {
|
||||||
const { presignedUrl, profileImageUrl } = preSignedResponse.data;
|
const { presignedUrl, profileImageUrl } = preSignedResponse;
|
||||||
|
|
||||||
const mimeType = await fileTypeFromFile(newProfileImagePath);
|
const mimeType = await fileTypeFromFile(newProfileImagePath);
|
||||||
|
|
||||||
@ -51,13 +49,11 @@ const updateProfile = async (
|
|||||||
"Content-Type": mimeType?.mime,
|
"Content-Type": mimeType?.mime,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return profileImageUrl;
|
return profileImageUrl as string;
|
||||||
})
|
})
|
||||||
.catch(() => undefined);
|
.catch(() => undefined);
|
||||||
|
|
||||||
return patchUserProfile(displayName, profileImageUrl).then(
|
return patchUserProfile(displayName, profileImageUrl);
|
||||||
(response) => response.data as UserProfile
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent("updateProfile", updateProfile);
|
registerEvent("updateProfile", updateProfile);
|
||||||
|
@ -10,8 +10,7 @@ const getUser = async (
|
|||||||
userId: string
|
userId: string
|
||||||
): Promise<UserProfile | null> => {
|
): Promise<UserProfile | null> => {
|
||||||
try {
|
try {
|
||||||
const response = await HydraApi.get(`/user/${userId}`);
|
const profile = await HydraApi.get(`/user/${userId}`);
|
||||||
const profile = response.data;
|
|
||||||
|
|
||||||
const recentGames = await Promise.all(
|
const recentGames = await Promise.all(
|
||||||
profile.recentGames.map(async (game) => {
|
profile.recentGames.map(async (game) => {
|
||||||
|
@ -20,6 +20,8 @@ autoUpdater.setFeedURL({
|
|||||||
|
|
||||||
autoUpdater.logger = logger;
|
autoUpdater.logger = logger;
|
||||||
|
|
||||||
|
logger.log("Init Hydra");
|
||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
if (!gotTheLock) app.quit();
|
if (!gotTheLock) app.quit();
|
||||||
|
|
||||||
@ -121,6 +123,7 @@ app.on("window-all-closed", () => {
|
|||||||
app.on("before-quit", () => {
|
app.on("before-quit", () => {
|
||||||
/* Disconnects libtorrent */
|
/* Disconnects libtorrent */
|
||||||
PythonInstance.kill();
|
PythonInstance.kill();
|
||||||
|
logger.log("Quit Hydra");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
|
@ -10,7 +10,7 @@ import { UserNotLoggedInError } from "@shared";
|
|||||||
export class HydraApi {
|
export class HydraApi {
|
||||||
private static instance: AxiosInstance;
|
private static instance: AxiosInstance;
|
||||||
|
|
||||||
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5;
|
private static readonly EXPIRATION_OFFSET_IN_MS = 1000 * 60 * 5; // 5 minutes
|
||||||
|
|
||||||
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
private static secondsToMilliseconds = (seconds: number) => seconds * 1000;
|
||||||
|
|
||||||
@ -45,6 +45,8 @@ export class HydraApi {
|
|||||||
expirationTimestamp: tokenExpirationTimestamp,
|
expirationTimestamp: tokenExpirationTimestamp,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.log("Sign in received", this.userAuth);
|
||||||
|
|
||||||
await userAuthRepository.upsert(
|
await userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -74,7 +76,7 @@ export class HydraApi {
|
|||||||
return request;
|
return request;
|
||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
logger.log("request error", error);
|
logger.error("request error", error);
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -95,12 +97,18 @@ export class HydraApi {
|
|||||||
|
|
||||||
const { config } = error;
|
const { config } = error;
|
||||||
|
|
||||||
logger.error(config.method, config.baseURL, config.url, config.headers);
|
logger.error(
|
||||||
|
config.method,
|
||||||
|
config.baseURL,
|
||||||
|
config.url,
|
||||||
|
config.headers,
|
||||||
|
config.data
|
||||||
|
);
|
||||||
|
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
logger.error(error.response.status, error.response.data);
|
logger.error("Response", error.response.status, error.response.data);
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
logger.error(error.request);
|
logger.error("Request", error.request);
|
||||||
} else {
|
} else {
|
||||||
logger.error("Error", error.message);
|
logger.error("Error", error.message);
|
||||||
}
|
}
|
||||||
@ -146,6 +154,8 @@ export class HydraApi {
|
|||||||
this.userAuth.authToken = accessToken;
|
this.userAuth.authToken = accessToken;
|
||||||
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
this.userAuth.expirationTimestamp = tokenExpirationTimestamp;
|
||||||
|
|
||||||
|
logger.log("Token refreshed", this.userAuth);
|
||||||
|
|
||||||
userAuthRepository.upsert(
|
userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
@ -170,6 +180,8 @@ export class HydraApi {
|
|||||||
|
|
||||||
private static handleUnauthorizedError = (err) => {
|
private static handleUnauthorizedError = (err) => {
|
||||||
if (err instanceof AxiosError && err.response?.status === 401) {
|
if (err instanceof AxiosError && err.response?.status === 401) {
|
||||||
|
logger.error("401 - Current credentials:", this.userAuth);
|
||||||
|
|
||||||
this.userAuth = {
|
this.userAuth = {
|
||||||
authToken: "",
|
authToken: "",
|
||||||
expirationTimestamp: 0,
|
expirationTimestamp: 0,
|
||||||
@ -190,6 +202,7 @@ export class HydraApi {
|
|||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.get(url, this.getAxiosConfig())
|
.get(url, this.getAxiosConfig())
|
||||||
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,6 +212,7 @@ export class HydraApi {
|
|||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.post(url, data, this.getAxiosConfig())
|
.post(url, data, this.getAxiosConfig())
|
||||||
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -208,6 +222,7 @@ export class HydraApi {
|
|||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.put(url, data, this.getAxiosConfig())
|
.put(url, data, this.getAxiosConfig())
|
||||||
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -217,6 +232,7 @@ export class HydraApi {
|
|||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.patch(url, data, this.getAxiosConfig())
|
.patch(url, data, this.getAxiosConfig())
|
||||||
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -226,6 +242,7 @@ export class HydraApi {
|
|||||||
await this.revalidateAccessTokenIfExpired();
|
await this.revalidateAccessTokenIfExpired();
|
||||||
return this.instance
|
return this.instance
|
||||||
.delete(url, this.getAxiosConfig())
|
.delete(url, this.getAxiosConfig())
|
||||||
|
.then((response) => response.data)
|
||||||
.catch(this.handleUnauthorizedError);
|
.catch(this.handleUnauthorizedError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,7 @@ export const createGame = async (game: Game) => {
|
|||||||
lastTimePlayed: game.lastTimePlayed,
|
lastTimePlayed: game.lastTimePlayed,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const {
|
const { id: remoteId, playTimeInMilliseconds, lastTimePlayed } = response;
|
||||||
id: remoteId,
|
|
||||||
playTimeInMilliseconds,
|
|
||||||
lastTimePlayed,
|
|
||||||
} = response.data;
|
|
||||||
|
|
||||||
gameRepository.update(
|
gameRepository.update(
|
||||||
{ objectID: game.objectID },
|
{ objectID: game.objectID },
|
||||||
|
@ -6,7 +6,7 @@ import { getSteamAppAsset } from "@main/helpers";
|
|||||||
export const mergeWithRemoteGames = async () => {
|
export const mergeWithRemoteGames = async () => {
|
||||||
return HydraApi.get("/games")
|
return HydraApi.get("/games")
|
||||||
.then(async (response) => {
|
.then(async (response) => {
|
||||||
for (const game of response.data) {
|
for (const game of response) {
|
||||||
const localGame = await gameRepository.findOne({
|
const localGame = await gameRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
objectID: game.objectId,
|
objectID: game.objectId,
|
||||||
|
@ -9,6 +9,7 @@ import type {
|
|||||||
AppUpdaterEvent,
|
AppUpdaterEvent,
|
||||||
StartGameDownloadPayload,
|
StartGameDownloadPayload,
|
||||||
GameRunning,
|
GameRunning,
|
||||||
|
FriendRequestAction,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("electron", {
|
contextBridge.exposeInMainWorld("electron", {
|
||||||
@ -136,6 +137,11 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
getMe: () => ipcRenderer.invoke("getMe"),
|
getMe: () => ipcRenderer.invoke("getMe"),
|
||||||
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
updateProfile: (displayName: string, newProfileImagePath: string | null) =>
|
||||||
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
ipcRenderer.invoke("updateProfile", displayName, newProfileImagePath),
|
||||||
|
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
|
||||||
|
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
|
||||||
|
ipcRenderer.invoke("updateFriendRequest", userId, action),
|
||||||
|
sendFriendRequest: (userId: string) =>
|
||||||
|
ipcRenderer.invoke("sendFriendRequest", userId),
|
||||||
|
|
||||||
/* User */
|
/* User */
|
||||||
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
getUser: (userId: string) => ipcRenderer.invoke("getUser", userId),
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<title>Hydra</title>
|
<title>Hydra</title>
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://*.cloudfront.net https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: https://cdn.discordapp.com https://*.cloudfront.net https://*.s3.amazonaws.com https://steamcdn-a.akamaihd.net https://shared.akamai.steamstatic.com https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com; media-src 'self' local: data: https://steamcdn-a.akamaihd.net https://cdn.cloudflare.steamstatic.com https://cdn2.steamgriddb.com https://cdn.akamai.steamstatic.com https://shared.akamai.steamstatic.com;"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body style="background-color: #1c1c1c">
|
<body style="background-color: #1c1c1c">
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
setGameRunning,
|
setGameRunning,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
||||||
|
|
||||||
export interface AppProps {
|
export interface AppProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@ -38,6 +39,13 @@ export function App() {
|
|||||||
|
|
||||||
const { clearDownload, setLastPacket } = useDownload();
|
const { clearDownload, setLastPacket } = useDownload();
|
||||||
|
|
||||||
|
const {
|
||||||
|
isFriendsModalVisible,
|
||||||
|
friendRequetsModalTab,
|
||||||
|
updateFriendRequests,
|
||||||
|
hideFriendsModal,
|
||||||
|
} = useUserDetails();
|
||||||
|
|
||||||
const { fetchUserDetails, updateUserDetails, clearUserDetails } =
|
const { fetchUserDetails, updateUserDetails, clearUserDetails } =
|
||||||
useUserDetails();
|
useUserDetails();
|
||||||
|
|
||||||
@ -94,7 +102,10 @@ export function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) updateUserDetails(response);
|
if (response) {
|
||||||
|
updateUserDetails(response);
|
||||||
|
updateFriendRequests();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
}, [fetchUserDetails, updateUserDetails, dispatch]);
|
||||||
|
|
||||||
@ -102,6 +113,7 @@ export function App() {
|
|||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
updateUserDetails(response);
|
updateUserDetails(response);
|
||||||
|
updateFriendRequests();
|
||||||
showSuccessToast(t("successfully_signed_in"));
|
showSuccessToast(t("successfully_signed_in"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -206,6 +218,12 @@ export function App() {
|
|||||||
onClose={handleToastClose}
|
onClose={handleToastClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UserFriendModal
|
||||||
|
visible={isFriendsModalVisible}
|
||||||
|
initialTab={friendRequetsModalTab}
|
||||||
|
onClose={hideFriendsModal}
|
||||||
|
/>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
|
@ -1,7 +1,18 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
import { createVar, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
|
export const profileContainerBackground = createVar();
|
||||||
|
|
||||||
|
export const profileContainer = style({
|
||||||
|
background: profileContainerBackground,
|
||||||
|
position: "relative",
|
||||||
|
cursor: "pointer",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const profileButton = style({
|
export const profileButton = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
@ -10,9 +21,8 @@ export const profileButton = style({
|
|||||||
color: vars.color.muted,
|
color: vars.color.muted,
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
borderBottom: `solid 1px ${vars.color.border}`,
|
||||||
boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)",
|
boxShadow: "0px 0px 15px 0px rgb(0 0 0 / 70%)",
|
||||||
":hover": {
|
width: "100%",
|
||||||
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
zIndex: "10",
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const profileButtonContent = style({
|
export const profileButtonContent = style({
|
||||||
@ -64,3 +74,25 @@ export const profileButtonTitle = style({
|
|||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const friendRequestContainer = style({
|
||||||
|
position: "absolute",
|
||||||
|
padding: "8px",
|
||||||
|
right: `${SPACING_UNIT}px`,
|
||||||
|
display: "flex",
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
alignItems: "center",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const friendRequestButton = style({
|
||||||
|
color: vars.color.success,
|
||||||
|
cursor: "pointer",
|
||||||
|
borderRadius: "50%",
|
||||||
|
overflow: "hidden",
|
||||||
|
width: "40px",
|
||||||
|
height: "40px",
|
||||||
|
":hover": {
|
||||||
|
color: vars.color.muted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
@ -1,17 +1,20 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { PersonIcon } from "@primer/octicons-react";
|
import { PersonAddIcon, PersonIcon } from "@primer/octicons-react";
|
||||||
import * as styles from "./sidebar-profile.css";
|
import * as styles from "./sidebar-profile.css";
|
||||||
|
import { assignInlineVars } from "@vanilla-extract/dynamic";
|
||||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { profileContainerBackground } from "./sidebar-profile.css";
|
||||||
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
|
||||||
export function SidebarProfile() {
|
export function SidebarProfile() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { t } = useTranslation("sidebar");
|
const { t } = useTranslation("sidebar");
|
||||||
|
|
||||||
const { userDetails, profileBackground } = useUserDetails();
|
const { userDetails, profileBackground, friendRequests, showFriendsModal } =
|
||||||
|
useUserDetails();
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||||
|
|
||||||
@ -30,46 +33,64 @@ export function SidebarProfile() {
|
|||||||
}, [profileBackground]);
|
}, [profileBackground]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
type="button"
|
className={styles.profileContainer}
|
||||||
className={styles.profileButton}
|
style={assignInlineVars({
|
||||||
style={{ background: profileButtonBackground }}
|
[profileContainerBackground]: profileButtonBackground,
|
||||||
onClick={handleButtonClick}
|
})}
|
||||||
>
|
>
|
||||||
<div className={styles.profileButtonContent}>
|
<button
|
||||||
<div className={styles.profileAvatar}>
|
type="button"
|
||||||
{userDetails?.profileImageUrl ? (
|
className={styles.profileButton}
|
||||||
<img
|
onClick={handleButtonClick}
|
||||||
className={styles.profileAvatar}
|
>
|
||||||
src={userDetails.profileImageUrl}
|
<div className={styles.profileButtonContent}>
|
||||||
alt={userDetails.displayName}
|
<div className={styles.profileAvatar}>
|
||||||
/>
|
{userDetails?.profileImageUrl ? (
|
||||||
) : (
|
<img
|
||||||
<PersonIcon />
|
className={styles.profileAvatar}
|
||||||
)}
|
src={userDetails.profileImageUrl}
|
||||||
</div>
|
alt={userDetails.displayName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.profileButtonInformation}>
|
<div className={styles.profileButtonInformation}>
|
||||||
<p className={styles.profileButtonTitle}>
|
<p className={styles.profileButtonTitle}>
|
||||||
{userDetails ? userDetails.displayName : t("sign_in")}
|
{userDetails ? userDetails.displayName : t("sign_in")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{userDetails && gameRunning && (
|
||||||
|
<div>
|
||||||
|
<small>{gameRunning.title}</small>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{userDetails && gameRunning && (
|
{userDetails && gameRunning && (
|
||||||
<div>
|
<img
|
||||||
<small>{gameRunning.title}</small>
|
alt={gameRunning.title}
|
||||||
</div>
|
width={24}
|
||||||
|
style={{ borderRadius: 4 }}
|
||||||
|
src={gameRunning.iconUrl}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</button>
|
||||||
{userDetails && gameRunning && (
|
{userDetails && friendRequests.length > 0 && !gameRunning && (
|
||||||
<img
|
<div className={styles.friendRequestContainer}>
|
||||||
alt={gameRunning.title}
|
<button
|
||||||
width={24}
|
type="button"
|
||||||
style={{ borderRadius: 4 }}
|
className={styles.friendRequestButton}
|
||||||
src={gameRunning.iconUrl}
|
onClick={() => showFriendsModal(UserFriendModalTab.AddFriend)}
|
||||||
/>
|
>
|
||||||
)}
|
<PersonAddIcon size={24} />
|
||||||
</div>
|
{friendRequests.length}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
8
src/renderer/src/declaration.d.ts
vendored
8
src/renderer/src/declaration.d.ts
vendored
@ -14,6 +14,8 @@ import type {
|
|||||||
RealDebridUser,
|
RealDebridUser,
|
||||||
DownloadSource,
|
DownloadSource,
|
||||||
UserProfile,
|
UserProfile,
|
||||||
|
FriendRequest,
|
||||||
|
FriendRequestAction,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import type { DiskSpace } from "check-disk-space";
|
import type { DiskSpace } from "check-disk-space";
|
||||||
|
|
||||||
@ -132,6 +134,12 @@ declare global {
|
|||||||
displayName: string,
|
displayName: string,
|
||||||
newProfileImagePath: string | null
|
newProfileImagePath: string | null
|
||||||
) => Promise<UserProfile>;
|
) => Promise<UserProfile>;
|
||||||
|
getFriendRequests: () => Promise<FriendRequest[]>;
|
||||||
|
updateFriendRequest: (
|
||||||
|
userId: string,
|
||||||
|
action: FriendRequestAction
|
||||||
|
) => Promise<void>;
|
||||||
|
sendFriendRequest: (userId: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -1,14 +1,21 @@
|
|||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import type { UserDetails } from "@types";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
import type { FriendRequest, UserDetails } from "@types";
|
||||||
|
|
||||||
export interface UserDetailsState {
|
export interface UserDetailsState {
|
||||||
userDetails: UserDetails | null;
|
userDetails: UserDetails | null;
|
||||||
profileBackground: null | string;
|
profileBackground: null | string;
|
||||||
|
friendRequests: FriendRequest[];
|
||||||
|
isFriendsModalVisible: boolean;
|
||||||
|
friendRequetsModalTab: UserFriendModalTab | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: UserDetailsState = {
|
const initialState: UserDetailsState = {
|
||||||
userDetails: null,
|
userDetails: null,
|
||||||
profileBackground: null,
|
profileBackground: null,
|
||||||
|
friendRequests: [],
|
||||||
|
isFriendsModalVisible: false,
|
||||||
|
friendRequetsModalTab: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userDetailsSlice = createSlice({
|
export const userDetailsSlice = createSlice({
|
||||||
@ -21,8 +28,27 @@ export const userDetailsSlice = createSlice({
|
|||||||
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
||||||
state.profileBackground = action.payload;
|
state.profileBackground = action.payload;
|
||||||
},
|
},
|
||||||
|
setFriendRequests: (state, action: PayloadAction<FriendRequest[]>) => {
|
||||||
|
state.friendRequests = action.payload;
|
||||||
|
},
|
||||||
|
setFriendsModalVisible: (
|
||||||
|
state,
|
||||||
|
action: PayloadAction<UserFriendModalTab>
|
||||||
|
) => {
|
||||||
|
state.isFriendsModalVisible = true;
|
||||||
|
state.friendRequetsModalTab = action.payload;
|
||||||
|
},
|
||||||
|
setFriendsModalHidden: (state) => {
|
||||||
|
state.isFriendsModalVisible = false;
|
||||||
|
state.friendRequetsModalTab = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { setUserDetails, setProfileBackground } =
|
export const {
|
||||||
userDetailsSlice.actions;
|
setUserDetails,
|
||||||
|
setProfileBackground,
|
||||||
|
setFriendRequests,
|
||||||
|
setFriendsModalVisible,
|
||||||
|
setFriendsModalHidden,
|
||||||
|
} = userDetailsSlice.actions;
|
||||||
|
@ -2,16 +2,27 @@ import { useCallback } from "react";
|
|||||||
import { average } from "color.js";
|
import { average } from "color.js";
|
||||||
|
|
||||||
import { useAppDispatch, useAppSelector } from "./redux";
|
import { useAppDispatch, useAppSelector } from "./redux";
|
||||||
import { setProfileBackground, setUserDetails } from "@renderer/features";
|
import {
|
||||||
|
setProfileBackground,
|
||||||
|
setUserDetails,
|
||||||
|
setFriendRequests,
|
||||||
|
setFriendsModalVisible,
|
||||||
|
setFriendsModalHidden,
|
||||||
|
} from "@renderer/features";
|
||||||
import { darkenColor } from "@renderer/helpers";
|
import { darkenColor } from "@renderer/helpers";
|
||||||
import { UserDetails } from "@types";
|
import { FriendRequestAction, UserDetails } from "@types";
|
||||||
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
|
||||||
export function useUserDetails() {
|
export function useUserDetails() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { userDetails, profileBackground } = useAppSelector(
|
const {
|
||||||
(state) => state.userDetails
|
userDetails,
|
||||||
);
|
profileBackground,
|
||||||
|
friendRequests,
|
||||||
|
isFriendsModalVisible,
|
||||||
|
friendRequetsModalTab,
|
||||||
|
} = useAppSelector((state) => state.userDetails);
|
||||||
|
|
||||||
const clearUserDetails = useCallback(async () => {
|
const clearUserDetails = useCallback(async () => {
|
||||||
dispatch(setUserDetails(null));
|
dispatch(setUserDetails(null));
|
||||||
@ -78,13 +89,56 @@ export function useUserDetails() {
|
|||||||
[updateUserDetails]
|
[updateUserDetails]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const updateFriendRequests = useCallback(async () => {
|
||||||
|
const friendRequests = await window.electron.getFriendRequests();
|
||||||
|
dispatch(setFriendRequests(friendRequests));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const showFriendsModal = useCallback(
|
||||||
|
(tab: UserFriendModalTab) => {
|
||||||
|
dispatch(setFriendsModalVisible(tab));
|
||||||
|
updateFriendRequests();
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hideFriendsModal = useCallback(() => {
|
||||||
|
dispatch(setFriendsModalHidden());
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const sendFriendRequest = useCallback(
|
||||||
|
async (userId: string) => {
|
||||||
|
return window.electron
|
||||||
|
.sendFriendRequest(userId)
|
||||||
|
.then(() => updateFriendRequests());
|
||||||
|
},
|
||||||
|
[updateFriendRequests]
|
||||||
|
);
|
||||||
|
|
||||||
|
const updateFriendRequestState = useCallback(
|
||||||
|
async (userId: string, action: FriendRequestAction) => {
|
||||||
|
return window.electron
|
||||||
|
.updateFriendRequest(userId, action)
|
||||||
|
.then(() => updateFriendRequests());
|
||||||
|
},
|
||||||
|
[updateFriendRequests]
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
userDetails,
|
userDetails,
|
||||||
|
profileBackground,
|
||||||
|
friendRequests,
|
||||||
|
friendRequetsModalTab,
|
||||||
|
isFriendsModalVisible,
|
||||||
|
showFriendsModal,
|
||||||
|
hideFriendsModal,
|
||||||
fetchUserDetails,
|
fetchUserDetails,
|
||||||
signOut,
|
signOut,
|
||||||
clearUserDetails,
|
clearUserDetails,
|
||||||
updateUserDetails,
|
updateUserDetails,
|
||||||
patchUser,
|
patchUser,
|
||||||
profileBackground,
|
sendFriendRequest,
|
||||||
|
updateFriendRequests,
|
||||||
|
updateFriendRequestState,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export * from "./user-friend-modal";
|
@ -0,0 +1,140 @@
|
|||||||
|
import { Button, TextField } from "@renderer/components";
|
||||||
|
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { UserFriendRequest } from "./user-friend-request";
|
||||||
|
|
||||||
|
export interface UserFriendModalAddFriendProps {
|
||||||
|
closeModal: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserFriendModalAddFriend = ({
|
||||||
|
closeModal,
|
||||||
|
}: UserFriendModalAddFriendProps) => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
const [friendCode, setFriendCode] = useState("");
|
||||||
|
const [isAddingFriend, setIsAddingFriend] = useState(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { sendFriendRequest, updateFriendRequestState, friendRequests } =
|
||||||
|
useUserDetails();
|
||||||
|
|
||||||
|
const { showErrorToast } = useToast();
|
||||||
|
|
||||||
|
const handleClickAddFriend = () => {
|
||||||
|
setIsAddingFriend(true);
|
||||||
|
sendFriendRequest(friendCode)
|
||||||
|
.then(() => {
|
||||||
|
// TODO: add validation for this input?
|
||||||
|
setFriendCode("");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showErrorToast("Não foi possível enviar o pedido de amizade");
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsAddingFriend(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetAndClose = () => {
|
||||||
|
setFriendCode("");
|
||||||
|
closeModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickRequest = (userId: string) => {
|
||||||
|
resetAndClose();
|
||||||
|
navigate(`/user/${userId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickSeeProfile = () => {
|
||||||
|
resetAndClose();
|
||||||
|
// TODO: add validation for this input?
|
||||||
|
navigate(`/user/${friendCode}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickCancelFriendRequest = (userId: string) => {
|
||||||
|
updateFriendRequestState(userId, "CANCEL").catch(() => {
|
||||||
|
showErrorToast("Falha ao cancelar convite");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickAcceptFriendRequest = (userId: string) => {
|
||||||
|
updateFriendRequestState(userId, "ACCEPTED").catch(() => {
|
||||||
|
showErrorToast("Falha ao aceitar convite");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClickRefuseFriendRequest = (userId: string) => {
|
||||||
|
updateFriendRequestState(userId, "REFUSED").catch(() => {
|
||||||
|
showErrorToast("Falha ao recusar convite");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TextField
|
||||||
|
label={t("friend_code")}
|
||||||
|
value={friendCode}
|
||||||
|
minLength={8}
|
||||||
|
maxLength={8}
|
||||||
|
containerProps={{ style: { width: "100%" } }}
|
||||||
|
onChange={(e) => setFriendCode(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
disabled={isAddingFriend}
|
||||||
|
style={{ alignSelf: "end" }}
|
||||||
|
type="button"
|
||||||
|
onClick={handleClickAddFriend}
|
||||||
|
>
|
||||||
|
{isAddingFriend ? t("sending") : t("add")}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleClickSeeProfile}
|
||||||
|
disabled={isAddingFriend}
|
||||||
|
style={{ alignSelf: "end" }}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{t("see_profile")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3>Pendentes</h3>
|
||||||
|
{friendRequests.map((request) => {
|
||||||
|
return (
|
||||||
|
<UserFriendRequest
|
||||||
|
key={request.id}
|
||||||
|
displayName={request.displayName}
|
||||||
|
isRequestSent={request.type === "SENT"}
|
||||||
|
profileImageUrl={request.profileImageUrl}
|
||||||
|
userId={request.id}
|
||||||
|
onClickAcceptRequest={handleClickAcceptFriendRequest}
|
||||||
|
onClickCancelRequest={handleClickCancelFriendRequest}
|
||||||
|
onClickRefuseRequest={handleClickRefuseFriendRequest}
|
||||||
|
onClickRequest={handleClickRequest}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,92 @@
|
|||||||
|
import { SPACING_UNIT, vars } from "../../../theme.css";
|
||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
export const profileContentBox = style({
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
width: "100%",
|
||||||
|
boxShadow: "0px 0px 15px 0px rgba(0, 0, 0, 0.7)",
|
||||||
|
transition: "all ease 0.3s",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const friendAvatarContainer = style({
|
||||||
|
width: "35px",
|
||||||
|
minWidth: "35px",
|
||||||
|
height: "35px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
overflow: "hidden",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const friendListDisplayName = style({
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: vars.size.body,
|
||||||
|
textAlign: "left",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileAvatar = style({
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const friendListContainer = style({
|
||||||
|
width: "100%",
|
||||||
|
height: "54px",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
position: "relative",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const friendListButton = style({
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
position: "absolute",
|
||||||
|
cursor: "pointer",
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
flexDirection: "row",
|
||||||
|
color: vars.color.body,
|
||||||
|
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||||
|
padding: `0 ${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const friendRequestItem = style({
|
||||||
|
color: vars.color.body,
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const acceptRequestButton = style({
|
||||||
|
cursor: "pointer",
|
||||||
|
color: vars.color.body,
|
||||||
|
width: "28px",
|
||||||
|
height: "28px",
|
||||||
|
":hover": {
|
||||||
|
color: vars.color.success,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cancelRequestButton = style({
|
||||||
|
cursor: "pointer",
|
||||||
|
color: vars.color.body,
|
||||||
|
width: "28px",
|
||||||
|
height: "28px",
|
||||||
|
":hover": {
|
||||||
|
color: vars.color.danger,
|
||||||
|
},
|
||||||
|
});
|
@ -0,0 +1,77 @@
|
|||||||
|
import { Button, Modal } from "@renderer/components";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend";
|
||||||
|
|
||||||
|
export enum UserFriendModalTab {
|
||||||
|
FriendsList,
|
||||||
|
AddFriend,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UserAddFriendsModalProps {
|
||||||
|
visible: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
initialTab: UserFriendModalTab | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserFriendModal = ({
|
||||||
|
visible,
|
||||||
|
onClose,
|
||||||
|
initialTab,
|
||||||
|
}: UserAddFriendsModalProps) => {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
const tabs = [t("friends_list"), t("add_friends")];
|
||||||
|
|
||||||
|
const [currentTab, setCurrentTab] = useState(
|
||||||
|
initialTab || UserFriendModalTab.FriendsList
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialTab != null) {
|
||||||
|
setCurrentTab(initialTab);
|
||||||
|
}
|
||||||
|
}, [initialTab]);
|
||||||
|
|
||||||
|
const renderTab = () => {
|
||||||
|
if (currentTab == UserFriendModalTab.FriendsList) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTab == UserFriendModalTab.AddFriend) {
|
||||||
|
return <UserFriendModalAddFriend closeModal={onClose} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal visible={visible} title={t("friends")} onClose={onClose}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
width: "500px",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<section style={{ display: "flex", gap: `${SPACING_UNIT}px` }}>
|
||||||
|
{tabs.map((tab, index) => {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
theme={index === currentTab ? "primary" : "outline"}
|
||||||
|
onClick={() => setCurrentTab(index)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</section>
|
||||||
|
<h2>{tabs[currentTab]}</h2>
|
||||||
|
{renderTab()}
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,97 @@
|
|||||||
|
import {
|
||||||
|
CheckCircleIcon,
|
||||||
|
PersonIcon,
|
||||||
|
XCircleIcon,
|
||||||
|
} from "@primer/octicons-react";
|
||||||
|
import * as styles from "./user-friend-modal.css";
|
||||||
|
import cn from "classnames";
|
||||||
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
|
||||||
|
export interface UserFriendRequestProps {
|
||||||
|
userId: string;
|
||||||
|
profileImageUrl: string | null;
|
||||||
|
displayName: string;
|
||||||
|
isRequestSent: boolean;
|
||||||
|
onClickCancelRequest: (userId: string) => void;
|
||||||
|
onClickAcceptRequest: (userId: string) => void;
|
||||||
|
onClickRefuseRequest: (userId: string) => void;
|
||||||
|
onClickRequest: (userId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserFriendRequest = ({
|
||||||
|
userId,
|
||||||
|
profileImageUrl,
|
||||||
|
displayName,
|
||||||
|
isRequestSent,
|
||||||
|
onClickCancelRequest,
|
||||||
|
onClickAcceptRequest,
|
||||||
|
onClickRefuseRequest,
|
||||||
|
onClickRequest,
|
||||||
|
}: UserFriendRequestProps) => {
|
||||||
|
return (
|
||||||
|
<div className={cn(styles.friendListContainer, styles.profileContentBox)}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.friendListButton}
|
||||||
|
onClick={() => onClickRequest(userId)}
|
||||||
|
>
|
||||||
|
<div className={styles.friendAvatarContainer}>
|
||||||
|
{profileImageUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.profileAvatar}
|
||||||
|
alt={displayName}
|
||||||
|
src={profileImageUrl}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
flex: "1",
|
||||||
|
minWidth: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className={styles.friendListDisplayName}>{displayName}</p>
|
||||||
|
<small>{isRequestSent ? "Pedido enviado" : "Pedido recebido"}</small>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
right: "8px",
|
||||||
|
display: "flex",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isRequestSent ? (
|
||||||
|
<button
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() => onClickCancelRequest(userId)}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className={styles.acceptRequestButton}
|
||||||
|
onClick={() => onClickAcceptRequest(userId)}
|
||||||
|
>
|
||||||
|
<CheckCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={styles.cancelRequestButton}
|
||||||
|
onClick={() => onClickRefuseRequest(userId)}
|
||||||
|
>
|
||||||
|
<XCircleIcon size={28} />
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,9 +1,8 @@
|
|||||||
import { UserGame, UserProfile } from "@types";
|
import { UserGame, UserProfile } from "@types";
|
||||||
import cn from "classnames";
|
import cn from "classnames";
|
||||||
|
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./user.css";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
||||||
import { useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import {
|
import {
|
||||||
@ -14,10 +13,11 @@ import {
|
|||||||
} from "@renderer/hooks";
|
} from "@renderer/hooks";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
|
import { buildGameDetailsPath, steamUrlBuilder } from "@renderer/helpers";
|
||||||
import { PersonIcon, TelescopeIcon } from "@primer/octicons-react";
|
import { PersonIcon, PlusIcon, TelescopeIcon } from "@primer/octicons-react";
|
||||||
import { Button, Link } from "@renderer/components";
|
import { Button, Link } from "@renderer/components";
|
||||||
import { UserEditProfileModal } from "./user-edit-modal";
|
import { UserEditProfileModal } from "./user-edit-modal";
|
||||||
import { UserSignOutModal } from "./user-signout-modal";
|
import { UserSignOutModal } from "./user-signout-modal";
|
||||||
|
import { UserFriendModalTab } from "../shared-modals/user-friend-modal";
|
||||||
|
|
||||||
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||||
|
|
||||||
@ -32,7 +32,13 @@ export function UserContent({
|
|||||||
}: ProfileContentProps) {
|
}: ProfileContentProps) {
|
||||||
const { t, i18n } = useTranslation("user_profile");
|
const { t, i18n } = useTranslation("user_profile");
|
||||||
|
|
||||||
const { userDetails, profileBackground, signOut } = useUserDetails();
|
const {
|
||||||
|
userDetails,
|
||||||
|
profileBackground,
|
||||||
|
signOut,
|
||||||
|
updateFriendRequests,
|
||||||
|
showFriendsModal,
|
||||||
|
} = useUserDetails();
|
||||||
const { showSuccessToast } = useToast();
|
const { showSuccessToast } = useToast();
|
||||||
|
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
@ -72,6 +78,10 @@ export function UserContent({
|
|||||||
setShowEditProfileModal(true);
|
setShowEditProfileModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnClickFriend = (userId: string) => {
|
||||||
|
navigate(`/user/${userId}`);
|
||||||
|
};
|
||||||
|
|
||||||
const handleConfirmSignout = async () => {
|
const handleConfirmSignout = async () => {
|
||||||
await signOut();
|
await signOut();
|
||||||
|
|
||||||
@ -82,6 +92,10 @@ export function UserContent({
|
|||||||
|
|
||||||
const isMe = userDetails?.id == userProfile.id;
|
const isMe = userDetails?.id == userProfile.id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMe) updateFriendRequests();
|
||||||
|
}, [isMe]);
|
||||||
|
|
||||||
const profileContentBoxBackground = useMemo(() => {
|
const profileContentBoxBackground = useMemo(() => {
|
||||||
if (profileBackground) return profileBackground;
|
if (profileBackground) return profileBackground;
|
||||||
/* TODO: Render background colors for other users */
|
/* TODO: Render background colors for other users */
|
||||||
@ -216,9 +230,11 @@ export function UserContent({
|
|||||||
<TelescopeIcon size={24} />
|
<TelescopeIcon size={24} />
|
||||||
</div>
|
</div>
|
||||||
<h2>{t("no_recent_activity_title")}</h2>
|
<h2>{t("no_recent_activity_title")}</h2>
|
||||||
<p style={{ fontFamily: "Fira Sans" }}>
|
{isMe && (
|
||||||
{t("no_recent_activity_description")}
|
<p style={{ fontFamily: "Fira Sans" }}>
|
||||||
</p>
|
{t("no_recent_activity_description")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<div
|
||||||
@ -259,55 +275,128 @@ export function UserContent({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(styles.contentSidebar, styles.profileGameSection)}>
|
<div className={styles.contentSidebar}>
|
||||||
<div
|
<div className={styles.profileGameSection}>
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h2>{t("library")}</h2>
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
display: "flex",
|
||||||
backgroundColor: vars.color.border,
|
alignItems: "center",
|
||||||
height: "1px",
|
justifyContent: "space-between",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
<h3 style={{ fontWeight: "400" }}>
|
<h2>{t("library")}</h2>
|
||||||
{userProfile.libraryGames.length}
|
|
||||||
</h3>
|
<div
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: vars.color.border,
|
||||||
|
height: "1px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<h3 style={{ fontWeight: "400" }}>
|
||||||
|
{userProfile.libraryGames.length}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<small>{t("total_play_time", { amount: formatPlayTime() })}</small>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: "grid",
|
||||||
|
gridTemplateColumns: "repeat(4, 1fr)",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{userProfile.libraryGames.map((game) => (
|
||||||
|
<button
|
||||||
|
key={game.objectID}
|
||||||
|
className={cn(styles.gameListItem, styles.profileContentBox)}
|
||||||
|
onClick={() => handleGameClick(game)}
|
||||||
|
title={game.title}
|
||||||
|
>
|
||||||
|
{game.iconUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.libraryGameIcon}
|
||||||
|
src={game.iconUrl}
|
||||||
|
alt={game.title}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SteamLogo className={styles.libraryGameIcon} />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small>{t("total_play_time", { amount: formatPlayTime() })}</small>
|
|
||||||
<div
|
{(isMe ||
|
||||||
style={{
|
(userProfile.friends && userProfile.friends.length > 0)) && (
|
||||||
display: "grid",
|
<div className={styles.friendsSection}>
|
||||||
gridTemplateColumns: "repeat(4, 1fr)",
|
|
||||||
gap: `${SPACING_UNIT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{userProfile.libraryGames.map((game) => (
|
|
||||||
<button
|
<button
|
||||||
key={game.objectID}
|
className={styles.friendsSectionHeader}
|
||||||
className={cn(styles.gameListItem, styles.profileContentBox)}
|
onClick={() => showFriendsModal(UserFriendModalTab.FriendsList)}
|
||||||
onClick={() => handleGameClick(game)}
|
|
||||||
title={game.title}
|
|
||||||
>
|
>
|
||||||
{game.iconUrl ? (
|
<h2>{t("friends")}</h2>
|
||||||
<img
|
|
||||||
className={styles.libraryGameIcon}
|
<div
|
||||||
src={game.iconUrl}
|
style={{
|
||||||
alt={game.title}
|
flex: 1,
|
||||||
/>
|
backgroundColor: vars.color.border,
|
||||||
) : (
|
height: "1px",
|
||||||
<SteamLogo className={styles.libraryGameIcon} />
|
}}
|
||||||
)}
|
/>
|
||||||
|
<h3 style={{ fontWeight: "400" }}>
|
||||||
|
{userProfile.friends.length}
|
||||||
|
</h3>
|
||||||
</button>
|
</button>
|
||||||
))}
|
|
||||||
</div>
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{userProfile.friends.map((friend) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={friend.id}
|
||||||
|
className={cn(
|
||||||
|
styles.profileContentBox,
|
||||||
|
styles.friendListContainer
|
||||||
|
)}
|
||||||
|
onClick={() => handleOnClickFriend(friend.id)}
|
||||||
|
>
|
||||||
|
<div className={styles.friendAvatarContainer}>
|
||||||
|
{friend.profileImageUrl ? (
|
||||||
|
<img
|
||||||
|
className={styles.friendProfileIcon}
|
||||||
|
src={friend.profileImageUrl}
|
||||||
|
alt={friend.displayName}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<PersonIcon size={24} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={styles.friendListDisplayName}>
|
||||||
|
{friend.displayName}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{isMe && (
|
||||||
|
<Button
|
||||||
|
theme="outline"
|
||||||
|
onClick={() =>
|
||||||
|
showFriendsModal(UserFriendModalTab.AddFriend)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PlusIcon /> {t("add")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -11,6 +11,7 @@ export const wrapper = style({
|
|||||||
|
|
||||||
export const profileContentBox = style({
|
export const profileContentBox = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
cursor: "pointer",
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
@ -35,6 +36,29 @@ export const profileAvatarContainer = style({
|
|||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const friendAvatarContainer = style({
|
||||||
|
width: "35px",
|
||||||
|
minWidth: "35px",
|
||||||
|
height: "35px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
overflow: "hidden",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const friendListDisplayName = style({
|
||||||
|
fontWeight: "bold",
|
||||||
|
fontSize: vars.size.body,
|
||||||
|
textAlign: "left",
|
||||||
|
overflow: "hidden",
|
||||||
|
textOverflow: "ellipsis",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
});
|
||||||
|
|
||||||
export const profileAvatarEditContainer = style({
|
export const profileAvatarEditContainer = style({
|
||||||
width: "128px",
|
width: "128px",
|
||||||
height: "128px",
|
height: "128px",
|
||||||
@ -53,8 +77,6 @@ export const profileAvatarEditContainer = style({
|
|||||||
export const profileAvatar = style({
|
export const profileAvatar = style({
|
||||||
height: "100%",
|
height: "100%",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
borderRadius: "50%",
|
|
||||||
overflow: "hidden",
|
|
||||||
objectFit: "cover",
|
objectFit: "cover",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -86,14 +108,36 @@ export const profileContent = style({
|
|||||||
|
|
||||||
export const profileGameSection = style({
|
export const profileGameSection = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const friendsSection = style({
|
||||||
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const friendsSectionHeader = style({
|
||||||
|
fontSize: vars.size.body,
|
||||||
|
color: vars.color.body,
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
|
":hover": {
|
||||||
|
color: vars.color.muted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const contentSidebar = style({
|
export const contentSidebar = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
"@media": {
|
"@media": {
|
||||||
"(min-width: 768px)": {
|
"(min-width: 768px)": {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@ -116,12 +160,17 @@ export const libraryGameIcon = style({
|
|||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const friendProfileIcon = style({
|
||||||
|
height: "100%",
|
||||||
|
});
|
||||||
|
|
||||||
export const feedItem = style({
|
export const feedItem = style({
|
||||||
color: vars.color.body,
|
color: vars.color.body,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
gap: `${SPACING_UNIT * 2}px`,
|
gap: `${SPACING_UNIT * 2}px`,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
overflow: "hidden",
|
||||||
height: "72px",
|
height: "72px",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
@ -143,6 +192,19 @@ export const gameListItem = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const friendListContainer = style({
|
||||||
|
color: vars.color.body,
|
||||||
|
width: "100%",
|
||||||
|
height: "54px",
|
||||||
|
padding: `0 ${SPACING_UNIT}px`,
|
||||||
|
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
position: "relative",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export const gameInformation = style({
|
export const gameInformation = style({
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
@ -2,18 +2,23 @@ import { UserProfile } from "@types";
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { useAppDispatch } from "@renderer/hooks";
|
import { useAppDispatch, useToast } from "@renderer/hooks";
|
||||||
import { UserSkeleton } from "./user-skeleton";
|
import { UserSkeleton } from "./user-skeleton";
|
||||||
import { UserContent } from "./user-content";
|
import { UserContent } from "./user-content";
|
||||||
import { SkeletonTheme } from "react-loading-skeleton";
|
import { SkeletonTheme } from "react-loading-skeleton";
|
||||||
import { vars } from "@renderer/theme.css";
|
import { vars } from "@renderer/theme.css";
|
||||||
import * as styles from "./user.css";
|
import * as styles from "./user.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const User = () => {
|
export const User = () => {
|
||||||
const { userId } = useParams();
|
const { userId } = useParams();
|
||||||
const [userProfile, setUserProfile] = useState<UserProfile>();
|
const [userProfile, setUserProfile] = useState<UserProfile>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
const { showErrorToast } = useToast();
|
||||||
|
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const getUserProfile = useCallback(() => {
|
const getUserProfile = useCallback(() => {
|
||||||
@ -22,10 +27,11 @@ export const User = () => {
|
|||||||
dispatch(setHeaderTitle(userProfile.displayName));
|
dispatch(setHeaderTitle(userProfile.displayName));
|
||||||
setUserProfile(userProfile);
|
setUserProfile(userProfile);
|
||||||
} else {
|
} else {
|
||||||
|
showErrorToast(t("user_not_found"));
|
||||||
navigate(-1);
|
navigate(-1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [dispatch, userId]);
|
}, [dispatch, userId, t]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUserProfile();
|
getUserProfile();
|
||||||
|
@ -10,6 +10,8 @@ export type GameStatus =
|
|||||||
|
|
||||||
export type GameShop = "steam" | "epic";
|
export type GameShop = "steam" | "epic";
|
||||||
|
|
||||||
|
export type FriendRequestAction = "ACCEPTED" | "REFUSED" | "CANCEL";
|
||||||
|
|
||||||
export interface SteamGenre {
|
export interface SteamGenre {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@ -269,14 +271,27 @@ export interface UserDetails {
|
|||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UserFriend {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
profileImageUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FriendRequest {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
profileImageUrl: string | null;
|
||||||
|
type: "SENT" | "RECEIVED";
|
||||||
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
username: string;
|
|
||||||
profileImageUrl: string | null;
|
profileImageUrl: string | null;
|
||||||
totalPlayTimeInSeconds: number;
|
totalPlayTimeInSeconds: number;
|
||||||
libraryGames: UserGame[];
|
libraryGames: UserGame[];
|
||||||
recentGames: UserGame[];
|
recentGames: UserGame[];
|
||||||
|
friends: UserFriend[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadSource {
|
export interface DownloadSource {
|
||||||
|
12
yarn.lock
12
yarn.lock
@ -2433,6 +2433,13 @@
|
|||||||
modern-ahocorasick "^1.0.0"
|
modern-ahocorasick "^1.0.0"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
|
|
||||||
|
"@vanilla-extract/dynamic@^2.1.1":
|
||||||
|
version "2.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vanilla-extract/dynamic/-/dynamic-2.1.1.tgz#bc93a577b127a7dcb6f254973d13a863029a7faf"
|
||||||
|
integrity sha512-iqf736036ujEIKsIq28UsBEMaLC2vR2DhwKyrG3NDb/fRy9qL9FKl1TqTtBV4daU30Uh3saeik4vRzN8bzQMbw==
|
||||||
|
dependencies:
|
||||||
|
"@vanilla-extract/private" "^1.0.5"
|
||||||
|
|
||||||
"@vanilla-extract/integration@^7.1.3":
|
"@vanilla-extract/integration@^7.1.3":
|
||||||
version "7.1.4"
|
version "7.1.4"
|
||||||
resolved "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-7.1.4.tgz"
|
resolved "https://registry.npmjs.org/@vanilla-extract/integration/-/integration-7.1.4.tgz"
|
||||||
@ -2456,6 +2463,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.4.tgz"
|
resolved "https://registry.npmjs.org/@vanilla-extract/private/-/private-1.0.4.tgz"
|
||||||
integrity sha512-8FGD6AejeC/nXcblgNCM5rnZb9KXa4WNkR03HCWtdJBpANjTgjHEglNLFnhuvdQ78tC6afaxBPI+g7F2NX3tgg==
|
integrity sha512-8FGD6AejeC/nXcblgNCM5rnZb9KXa4WNkR03HCWtdJBpANjTgjHEglNLFnhuvdQ78tC6afaxBPI+g7F2NX3tgg==
|
||||||
|
|
||||||
|
"@vanilla-extract/private@^1.0.5":
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vanilla-extract/private/-/private-1.0.5.tgz#8c08ac4851f4cc89a3dcdb858d8938e69b1481c4"
|
||||||
|
integrity sha512-6YXeOEKYTA3UV+RC8DeAjFk+/okoNz/h88R+McnzA2zpaVqTR/Ep+vszkWYlGBcMNO7vEkqbq5nT/JMMvhi+tw==
|
||||||
|
|
||||||
"@vanilla-extract/recipes@^0.5.2":
|
"@vanilla-extract/recipes@^0.5.2":
|
||||||
version "0.5.2"
|
version "0.5.2"
|
||||||
resolved "https://registry.npmjs.org/@vanilla-extract/recipes/-/recipes-0.5.2.tgz"
|
resolved "https://registry.npmjs.org/@vanilla-extract/recipes/-/recipes-0.5.2.tgz"
|
||||||
|
Loading…
Reference in New Issue
Block a user