mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-03-09 15:40:26 +00:00
feat: profile redesign
This commit is contained in:
parent
8f0003298f
commit
d9a7672113
24 changed files with 268 additions and 97 deletions
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hydralauncher",
|
"name": "hydralauncher",
|
||||||
"version": "2.0.3",
|
"version": "2.1.0",
|
||||||
"description": "Hydra",
|
"description": "Hydra",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "Los Broxas",
|
"author": "Los Broxas",
|
||||||
|
|
|
@ -15,7 +15,8 @@ const getCatalogue = async (
|
||||||
});
|
});
|
||||||
|
|
||||||
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
|
const response = await HydraApi.get<{ objectId: string; shop: GameShop }[]>(
|
||||||
`/games/${category}?${params.toString()}`
|
`/games/${category}?${params.toString()}`,
|
||||||
|
{ needsAuth: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
|
|
|
@ -1,15 +1,33 @@
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import * as Sentry from "@sentry/electron/main";
|
import * as Sentry from "@sentry/electron/main";
|
||||||
import { HydraApi } from "@main/services";
|
import { HydraApi, logger } from "@main/services";
|
||||||
import { UserProfile } from "@types";
|
import { UserProfile } from "@types";
|
||||||
import { userAuthRepository } from "@main/repository";
|
import { userAuthRepository } from "@main/repository";
|
||||||
import { UserNotLoggedInError } from "@shared";
|
import { steamUrlBuilder, UserNotLoggedInError } from "@shared";
|
||||||
|
import { steamGamesWorker } from "@main/workers";
|
||||||
|
|
||||||
|
const getSteamGame = async (objectId: string) => {
|
||||||
|
try {
|
||||||
|
const steamGame = await steamGamesWorker.run(Number(objectId), {
|
||||||
|
name: "getById",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: steamGame.name,
|
||||||
|
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Failed to get Steam game", err);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getMe = async (
|
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((me) => {
|
.then(async (me) => {
|
||||||
userAuthRepository.upsert(
|
userAuthRepository.upsert(
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
|
@ -20,6 +38,17 @@ const getMe = async (
|
||||||
["id"]
|
["id"]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (me.currentGame) {
|
||||||
|
const steamGame = await getSteamGame(me.currentGame.objectId);
|
||||||
|
|
||||||
|
if (steamGame) {
|
||||||
|
me.currentGame = {
|
||||||
|
...me.currentGame,
|
||||||
|
...steamGame,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Sentry.setUser({ id: me.id, username: me.username });
|
Sentry.setUser({ id: me.id, username: me.username });
|
||||||
|
|
||||||
return me;
|
return me;
|
||||||
|
|
|
@ -81,7 +81,6 @@ const getUser = async (
|
||||||
totalFriends: friends.totalFriends,
|
totalFriends: friends.totalFriends,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log("err", err);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import { Game } from "@main/entity";
|
import { Game } from "@main/entity";
|
||||||
import { HydraApi } from "../hydra-api";
|
import { HydraApi } from "../hydra-api";
|
||||||
import { gameRepository } from "@main/repository";
|
import { gameRepository } from "@main/repository";
|
||||||
|
import { logger } from "../logger";
|
||||||
|
|
||||||
export const createGame = async (game: Game) => {
|
export const createGame = async (game: Game) => {
|
||||||
console.log({ objectId: game.objectID, shop: game.shop });
|
|
||||||
|
|
||||||
HydraApi.post("/games/download", {
|
HydraApi.post("/games/download", {
|
||||||
objectId: game.objectID,
|
objectId: game.objectID,
|
||||||
shop: game.shop,
|
shop: game.shop,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.log(err);
|
logger.error("Failed to create game download", err);
|
||||||
});
|
});
|
||||||
|
|
||||||
HydraApi.post(`/profile/games`, {
|
HydraApi.post(`/profile/games`, {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT } from "../../theme.css";
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
export const actions = style({
|
export const actions = style({
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
import {
|
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
|
||||||
DownloadIcon,
|
|
||||||
FileDirectoryIcon,
|
|
||||||
PeopleIcon,
|
|
||||||
} from "@primer/octicons-react";
|
|
||||||
import type { CatalogueEntry, GameStats } from "@types";
|
import type { CatalogueEntry, GameStats } from "@types";
|
||||||
|
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
|
import { PeopleIcon, PersonIcon } from "@primer/octicons-react";
|
||||||
import * as styles from "./sidebar-profile.css";
|
import * as styles from "./sidebar-profile.css";
|
||||||
import { useAppSelector, useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
|
@ -17,8 +17,6 @@ export function SidebarProfile() {
|
||||||
return friendRequests.filter((request) => request.type === "RECEIVED");
|
return friendRequests.filter((request) => request.type === "RECEIVED");
|
||||||
}, [friendRequests]);
|
}, [friendRequests]);
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
|
||||||
|
|
||||||
const handleProfileClick = () => {
|
const handleProfileClick = () => {
|
||||||
if (userDetails === null) {
|
if (userDetails === null) {
|
||||||
window.electron.openAuthWindow();
|
window.electron.openAuthWindow();
|
||||||
|
@ -76,19 +74,19 @@ export function SidebarProfile() {
|
||||||
{userDetails ? userDetails.displayName : t("sign_in")}
|
{userDetails ? userDetails.displayName : t("sign_in")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{userDetails && gameRunning && (
|
{userDetails && userDetails.currentGame && (
|
||||||
<div>
|
<div>
|
||||||
<small>{gameRunning.title}</small>
|
<small>{userDetails.currentGame.title}</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{userDetails && gameRunning?.iconUrl && (
|
{userDetails && userDetails.currentGame && (
|
||||||
<img
|
<img
|
||||||
alt={gameRunning.title}
|
alt={userDetails.currentGame.title}
|
||||||
width={24}
|
width={24}
|
||||||
style={{ borderRadius: 4 }}
|
style={{ borderRadius: 4 }}
|
||||||
src={gameRunning.iconUrl}
|
src={userDetails.currentGame.iconUrl!}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,7 +15,6 @@ import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { SidebarProfile } from "./sidebar-profile";
|
import { SidebarProfile } from "./sidebar-profile";
|
||||||
import { sortBy } from "lodash-es";
|
import { sortBy } from "lodash-es";
|
||||||
import { ChevronDownIcon } from "@primer/octicons-react";
|
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
|
|
|
@ -5,7 +5,13 @@ import { setHeaderTitle } from "@renderer/features";
|
||||||
import { getSteamLanguage } from "@renderer/helpers";
|
import { getSteamLanguage } from "@renderer/helpers";
|
||||||
import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks";
|
import { useAppDispatch, useAppSelector, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
|
import type {
|
||||||
|
Game,
|
||||||
|
GameRepack,
|
||||||
|
GameShop,
|
||||||
|
GameStats,
|
||||||
|
ShopDetails,
|
||||||
|
} from "@types";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { GameDetailsContext } from "./game-details.context.types";
|
import { GameDetailsContext } from "./game-details.context.types";
|
||||||
|
@ -22,6 +28,7 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
|
||||||
gameColor: "",
|
gameColor: "",
|
||||||
showRepacksModal: false,
|
showRepacksModal: false,
|
||||||
showGameOptionsModal: false,
|
showGameOptionsModal: false,
|
||||||
|
stats: null,
|
||||||
setGameColor: () => {},
|
setGameColor: () => {},
|
||||||
selectGameExecutable: async () => null,
|
selectGameExecutable: async () => null,
|
||||||
updateGame: async () => {},
|
updateGame: async () => {},
|
||||||
|
@ -45,6 +52,8 @@ export function GameDetailsContextProvider({
|
||||||
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||||
const [game, setGame] = useState<Game | null>(null);
|
const [game, setGame] = useState<Game | null>(null);
|
||||||
|
|
||||||
|
const [stats, setStats] = useState<GameStats | null>(null);
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [gameColor, setGameColor] = useState("");
|
const [gameColor, setGameColor] = useState("");
|
||||||
const [isGameRunning, setisGameRunning] = useState(false);
|
const [isGameRunning, setisGameRunning] = useState(false);
|
||||||
|
@ -85,13 +94,16 @@ export function GameDetailsContextProvider({
|
||||||
getSteamLanguage(i18n.language)
|
getSteamLanguage(i18n.language)
|
||||||
),
|
),
|
||||||
window.electron.searchGameRepacks(gameTitle),
|
window.electron.searchGameRepacks(gameTitle),
|
||||||
|
window.electron.getGameStats(objectID!, shop as GameShop),
|
||||||
])
|
])
|
||||||
.then(([appDetailsResult, repacksResult]) => {
|
.then(([appDetailsResult, repacksResult, statsResult]) => {
|
||||||
if (appDetailsResult.status === "fulfilled")
|
if (appDetailsResult.status === "fulfilled")
|
||||||
setGameDetails(appDetailsResult.value);
|
setGameDetails(appDetailsResult.value);
|
||||||
|
|
||||||
if (repacksResult.status === "fulfilled")
|
if (repacksResult.status === "fulfilled")
|
||||||
setRepacks(repacksResult.value);
|
setRepacks(repacksResult.value);
|
||||||
|
|
||||||
|
if (statsResult.status === "fulfilled") setStats(statsResult.value);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
@ -167,6 +179,7 @@ export function GameDetailsContextProvider({
|
||||||
gameColor,
|
gameColor,
|
||||||
showGameOptionsModal,
|
showGameOptionsModal,
|
||||||
showRepacksModal,
|
showRepacksModal,
|
||||||
|
stats,
|
||||||
setGameColor,
|
setGameColor,
|
||||||
selectGameExecutable,
|
selectGameExecutable,
|
||||||
updateGame,
|
updateGame,
|
||||||
|
|
|
@ -1,4 +1,10 @@
|
||||||
import type { Game, GameRepack, GameShop, ShopDetails } from "@types";
|
import type {
|
||||||
|
Game,
|
||||||
|
GameRepack,
|
||||||
|
GameShop,
|
||||||
|
GameStats,
|
||||||
|
ShopDetails,
|
||||||
|
} from "@types";
|
||||||
|
|
||||||
export interface GameDetailsContext {
|
export interface GameDetailsContext {
|
||||||
game: Game | null;
|
game: Game | null;
|
||||||
|
@ -12,6 +18,7 @@ export interface GameDetailsContext {
|
||||||
gameColor: string;
|
gameColor: string;
|
||||||
showRepacksModal: boolean;
|
showRepacksModal: boolean;
|
||||||
showGameOptionsModal: boolean;
|
showGameOptionsModal: boolean;
|
||||||
|
stats: GameStats | null;
|
||||||
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
setGameColor: React.Dispatch<React.SetStateAction<string>>;
|
||||||
selectGameExecutable: () => Promise<string | null>;
|
selectGameExecutable: () => Promise<string | null>;
|
||||||
updateGame: () => Promise<void>;
|
updateGame: () => Promise<void>;
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
import type { FriendRequest, UserDetails } from "@types";
|
import type { FriendRequest, UserProfile } from "@types";
|
||||||
|
|
||||||
export interface UserDetailsState {
|
export interface UserDetailsState {
|
||||||
userDetails: UserDetails | null;
|
userDetails: UserProfile | null;
|
||||||
profileBackground: null | string;
|
profileBackground: null | string;
|
||||||
friendRequests: FriendRequest[];
|
friendRequests: FriendRequest[];
|
||||||
isFriendsModalVisible: boolean;
|
isFriendsModalVisible: boolean;
|
||||||
|
@ -24,7 +24,7 @@ export const userDetailsSlice = createSlice({
|
||||||
name: "user-details",
|
name: "user-details",
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
setUserDetails: (state, action: PayloadAction<UserDetails | null>) => {
|
setUserDetails: (state, action: PayloadAction<UserProfile | null>) => {
|
||||||
state.userDetails = action.payload;
|
state.userDetails = action.payload;
|
||||||
},
|
},
|
||||||
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
setProfileBackground: (state, action: PayloadAction<string | null>) => {
|
||||||
|
|
|
@ -7,14 +7,12 @@ import {
|
||||||
setFriendsModalVisible,
|
setFriendsModalVisible,
|
||||||
setFriendsModalHidden,
|
setFriendsModalHidden,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
// import { profileBackgroundFromProfileImage } from "@renderer/helpers";
|
|
||||||
import type {
|
import type {
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
UpdateProfileRequest,
|
UpdateProfileRequest,
|
||||||
UserDetails,
|
UserProfile,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
||||||
import { logger } from "@renderer/logger";
|
|
||||||
|
|
||||||
export function useUserDetails() {
|
export function useUserDetails() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -42,17 +40,18 @@ export function useUserDetails() {
|
||||||
}, [clearUserDetails]);
|
}, [clearUserDetails]);
|
||||||
|
|
||||||
const updateUserDetails = useCallback(
|
const updateUserDetails = useCallback(
|
||||||
async (userDetails: UserDetails) => {
|
async (userDetails: UserProfile) => {
|
||||||
dispatch(setUserDetails(userDetails));
|
dispatch(setUserDetails(userDetails));
|
||||||
|
|
||||||
if (userDetails.profileImageUrl) {
|
if (userDetails.profileImageUrl) {
|
||||||
|
// TODO: Decide if we want to use this
|
||||||
// const profileBackground = await profileBackgroundFromProfileImage(
|
// const profileBackground = await profileBackgroundFromProfileImage(
|
||||||
// userDetails.profileImageUrl
|
// userDetails.profileImageUrl
|
||||||
// ).catch((err) => {
|
// ).catch((err) => {
|
||||||
// logger.error("profileBackgroundFromProfileImage", err);
|
// logger.error("profileBackgroundFromProfileImage", err);
|
||||||
// return `#151515B3`;
|
// return `#151515B3`;
|
||||||
// });
|
// });
|
||||||
dispatch(setProfileBackground(profileBackground));
|
// dispatch(setProfileBackground(profileBackground));
|
||||||
|
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
"userDetails",
|
"userDetails",
|
||||||
|
@ -68,7 +67,7 @@ export function useUserDetails() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch, profileBackground]
|
||||||
);
|
);
|
||||||
|
|
||||||
const fetchUserDetails = useCallback(async () => {
|
const fetchUserDetails = useCallback(async () => {
|
||||||
|
@ -83,7 +82,6 @@ export function useUserDetails() {
|
||||||
|
|
||||||
const patchUser = useCallback(
|
const patchUser = useCallback(
|
||||||
async (values: UpdateProfileRequest) => {
|
async (values: UpdateProfileRequest) => {
|
||||||
console.log("values", values);
|
|
||||||
const response = await window.electron.updateProfile(values);
|
const response = await window.electron.updateProfile(values);
|
||||||
return updateUserDetails(response);
|
return updateUserDetails(response);
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,7 +16,8 @@ export function Sidebar() {
|
||||||
const [activeRequirement, setActiveRequirement] =
|
const [activeRequirement, setActiveRequirement] =
|
||||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
||||||
|
|
||||||
const { gameTitle, shopDetails, objectID } = useContext(gameDetailsContext);
|
const { gameTitle, shopDetails, objectID, stats } =
|
||||||
|
useContext(gameDetailsContext);
|
||||||
|
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
@ -42,6 +43,15 @@ export function Sidebar() {
|
||||||
isLoading={howLongToBeat.isLoading}
|
isLoading={howLongToBeat.isLoading}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<div className={styles.contentSidebarTitle} style={{ border: "none" }}>
|
||||||
|
<h3>{t("stats")}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p>downloadCount {stats?.downloadCount}</p>
|
||||||
|
<p>playerCount {stats?.playerCount}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={styles.contentSidebarTitle} style={{ border: "none" }}>
|
<div className={styles.contentSidebarTitle} style={{ border: "none" }}>
|
||||||
<h3>{t("requirements")}</h3>
|
<h3>{t("requirements")}</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { SPACING_UNIT } from "../../../theme.css";
|
||||||
|
|
||||||
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
|
export const container = style({
|
||||||
|
display: "flex",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: `${SPACING_UNIT}px`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const lockIcon = style({
|
||||||
|
width: "60px",
|
||||||
|
height: "60px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "rgba(255, 255, 255, 0.06)",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
marginBottom: `${SPACING_UNIT * 2}px`,
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { LockIcon } from "@primer/octicons-react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import * as styles from "./locked-profile.css";
|
||||||
|
|
||||||
|
export function LockedProfile() {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.lockIcon}>
|
||||||
|
<LockIcon size={24} />
|
||||||
|
</div>
|
||||||
|
<h2>{t("locked_profile")}</h2>
|
||||||
|
<p>{t("locked_profile_description")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ import { ProfileHero } from "../profile-hero/profile-hero";
|
||||||
import { useAppDispatch } from "@renderer/hooks";
|
import { useAppDispatch } from "@renderer/hooks";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import { steamUrlBuilder } from "@shared";
|
import { steamUrlBuilder } from "@shared";
|
||||||
import { SPACING_UNIT, vars } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
|
||||||
import * as styles from "./profile-content.css";
|
import * as styles from "./profile-content.css";
|
||||||
import { ClockIcon } from "@primer/octicons-react";
|
import { ClockIcon } from "@primer/octicons-react";
|
||||||
|
@ -14,6 +14,7 @@ import { UserGame } from "@types";
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { LockedProfile } from "./locked-profile";
|
||||||
|
|
||||||
export function ProfileContent() {
|
export function ProfileContent() {
|
||||||
const { userProfile } = useContext(userProfileContext);
|
const { userProfile } = useContext(userProfileContext);
|
||||||
|
@ -64,12 +65,14 @@ export function ProfileContent() {
|
||||||
objectID: game.objectId,
|
objectID: game.objectId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!userProfile) return null;
|
const content = useMemo(() => {
|
||||||
|
if (!userProfile) return null;
|
||||||
|
|
||||||
return (
|
if (userProfile?.profileVisibility === "FRIENDS") {
|
||||||
<div>
|
return <LockedProfile />;
|
||||||
<ProfileHero />
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<section
|
<section
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
|
@ -133,7 +136,7 @@ export function ProfileContent() {
|
||||||
className={styles.listItem}
|
className={styles.listItem}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={game.iconUrl}
|
src={game.iconUrl!}
|
||||||
alt={game.title}
|
alt={game.title}
|
||||||
style={{
|
style={{
|
||||||
width: "30px",
|
width: "30px",
|
||||||
|
@ -184,7 +187,7 @@ export function ProfileContent() {
|
||||||
className={styles.listItem}
|
className={styles.listItem}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={friend.profileImageUrl}
|
src={friend.profileImageUrl!}
|
||||||
alt={friend.displayName}
|
alt={friend.displayName}
|
||||||
style={{
|
style={{
|
||||||
width: "30px",
|
width: "30px",
|
||||||
|
@ -203,6 +206,21 @@ export function ProfileContent() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
userProfile,
|
||||||
|
formatPlayTime,
|
||||||
|
numberFormatter,
|
||||||
|
t,
|
||||||
|
truncatedGamesList,
|
||||||
|
navigate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ProfileHero />
|
||||||
|
|
||||||
|
{content}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
|
|
||||||
import * as styles from "./profile-hero.css";
|
import * as styles from "./profile-hero.css";
|
||||||
import { useContext, useMemo, useState } from "react";
|
import { useCallback, useContext, useMemo, useState } from "react";
|
||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import {
|
import {
|
||||||
CheckCircleFillIcon,
|
CheckCircleFillIcon,
|
||||||
|
@ -48,39 +48,53 @@ export function ProfileHero() {
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = useCallback(async () => {
|
||||||
await signOut();
|
await signOut();
|
||||||
|
|
||||||
showSuccessToast(t("successfully_signed_out"));
|
showSuccessToast(t("successfully_signed_out"));
|
||||||
navigate("/");
|
navigate("/");
|
||||||
};
|
}, [navigate, signOut, showSuccessToast, t]);
|
||||||
|
|
||||||
const handleFriendAction = (userId: string, action: FriendAction) => {
|
const handleFriendAction = useCallback(
|
||||||
try {
|
(userId: string, action: FriendAction) => {
|
||||||
if (action === "UNDO_FRIENDSHIP") {
|
try {
|
||||||
undoFriendship(userId).then(getUserProfile);
|
if (action === "UNDO_FRIENDSHIP") {
|
||||||
return;
|
undoFriendship(userId).then(getUserProfile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "BLOCK") {
|
||||||
|
blockUser(userId).then(() => {
|
||||||
|
showSuccessToast(t("user_blocked_successfully"));
|
||||||
|
navigate(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === "SEND") {
|
||||||
|
sendFriendRequest(userProfile.id).then(getUserProfile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFriendRequestState(userId, action).then(getUserProfile);
|
||||||
|
} catch (err) {
|
||||||
|
showErrorToast(t("try_again"));
|
||||||
}
|
}
|
||||||
|
},
|
||||||
if (action === "BLOCK") {
|
[
|
||||||
blockUser(userId).then(() => {
|
undoFriendship,
|
||||||
showSuccessToast(t("user_blocked_successfully"));
|
blockUser,
|
||||||
navigate(-1);
|
sendFriendRequest,
|
||||||
});
|
updateFriendRequestState,
|
||||||
|
t,
|
||||||
return;
|
showErrorToast,
|
||||||
}
|
getUserProfile,
|
||||||
|
navigate,
|
||||||
if (action === "SEND") {
|
showSuccessToast,
|
||||||
sendFriendRequest(userProfile.id).then(getUserProfile);
|
userProfile.id,
|
||||||
return;
|
]
|
||||||
}
|
);
|
||||||
|
|
||||||
updateFriendRequestState(userId, action).then(getUserProfile);
|
|
||||||
} catch (err) {
|
|
||||||
showErrorToast(t("try_again"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const profileActions = useMemo(() => {
|
const profileActions = useMemo(() => {
|
||||||
if (isMe) {
|
if (isMe) {
|
||||||
|
@ -139,7 +153,7 @@ export function ProfileHero() {
|
||||||
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
handleFriendAction(userProfile.relation!.BId, "CANCEL")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<XCircleFillIcon size={28} /> {t("cancel_request")}
|
<XCircleFillIcon /> {t("cancel_request")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -152,7 +166,7 @@ export function ProfileHero() {
|
||||||
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
handleFriendAction(userProfile.relation!.AId, "ACCEPTED")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CheckCircleFillIcon size={28} /> {t("accept_request")}
|
<CheckCircleFillIcon /> {t("accept_request")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
|
@ -160,11 +174,17 @@ export function ProfileHero() {
|
||||||
handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
handleFriendAction(userProfile.relation!.AId, "REFUSED")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<XCircleFillIcon size={28} /> {t("ignore_request")}
|
<XCircleFillIcon /> {t("ignore_request")}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}, []);
|
}, [handleFriendAction, handleSignOut, isMe, t, userProfile]);
|
||||||
|
|
||||||
|
const handleAvatarClick = useCallback(() => {
|
||||||
|
if (isMe) {
|
||||||
|
setShowEditProfileModal(true);
|
||||||
|
}
|
||||||
|
}, [isMe]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -188,7 +208,11 @@ export function ProfileHero() {
|
||||||
style={{ background: heroBackground }}
|
style={{ background: heroBackground }}
|
||||||
>
|
>
|
||||||
<div className={styles.userInformation}>
|
<div className={styles.userInformation}>
|
||||||
<button type="button" className={styles.profileAvatarButton}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={styles.profileAvatarButton}
|
||||||
|
onClick={handleAvatarClick}
|
||||||
|
>
|
||||||
{userProfile.profileImageUrl ? (
|
{userProfile.profileImageUrl ? (
|
||||||
<img
|
<img
|
||||||
className={styles.profileAvatar}
|
className={styles.profileAvatar}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
import cn from "classnames";
|
|
||||||
import * as styles from "./profile.css";
|
|
||||||
import { SPACING_UNIT } from "@renderer/theme.css";
|
import { SPACING_UNIT } from "@renderer/theme.css";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
@ -9,9 +8,9 @@ export function ProfileSkeleton() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Skeleton className={styles.profileHeaderSkeleton} />
|
<Skeleton />
|
||||||
<div className={styles.profileContent}>
|
<div>
|
||||||
<div className={styles.profileGameSection}>
|
<div>
|
||||||
<h2>{t("activity")}</h2>
|
<h2>{t("activity")}</h2>
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
|
@ -22,7 +21,7 @@ export function ProfileSkeleton() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn(styles.contentSidebar, styles.profileGameSection)}>
|
<div>
|
||||||
<h2>{t("library")}</h2>
|
<h2>{t("library")}</h2>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { SPACING_UNIT } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
import { style } from "@vanilla-extract/css";
|
import { globalStyle, style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
export const wrapper = style({
|
export const wrapper = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
@ -7,3 +7,47 @@ export const wrapper = style({
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: `${SPACING_UNIT * 3}px`,
|
gap: `${SPACING_UNIT * 3}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Legacy styles */
|
||||||
|
export const profileAvatarEditContainer = style({
|
||||||
|
alignSelf: "center",
|
||||||
|
width: "128px",
|
||||||
|
height: "128px",
|
||||||
|
display: "flex",
|
||||||
|
borderRadius: "4px",
|
||||||
|
color: vars.color.body,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
backgroundColor: vars.color.background,
|
||||||
|
position: "relative",
|
||||||
|
border: `solid 1px ${vars.color.border}`,
|
||||||
|
boxShadow: "0px 0px 5px 0px rgba(0, 0, 0, 0.7)",
|
||||||
|
cursor: "pointer",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileAvatar = style({
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
objectFit: "cover",
|
||||||
|
borderRadius: "4px",
|
||||||
|
overflow: "hidden",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const profileAvatarEditOverlay = style({
|
||||||
|
position: "absolute",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||||
|
color: vars.color.muted,
|
||||||
|
zIndex: "1",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
transition: "all ease 0.2s",
|
||||||
|
alignItems: "center",
|
||||||
|
opacity: "0",
|
||||||
|
});
|
||||||
|
|
||||||
|
globalStyle(`${profileAvatarEditContainer}:hover ${profileAvatarEditOverlay}`, {
|
||||||
|
opacity: "1",
|
||||||
|
});
|
||||||
|
|
|
@ -116,8 +116,9 @@ export const UserEditProfile = ({
|
||||||
) : (
|
) : (
|
||||||
<PersonIcon size={96} />
|
<PersonIcon size={96} />
|
||||||
)}
|
)}
|
||||||
<div className={styles.editProfileImageBadge}>
|
|
||||||
<DeviceCameraIcon size={16} />
|
<div className={styles.profileAvatarEditOverlay}>
|
||||||
|
<DeviceCameraIcon size={38} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
@ -42,13 +42,13 @@ export const UserFriendModalAddFriend = ({
|
||||||
|
|
||||||
const handleClickRequest = (userId: string) => {
|
const handleClickRequest = (userId: string) => {
|
||||||
closeModal();
|
closeModal();
|
||||||
navigate(`/user/${userId}`);
|
navigate(`/profile/${userId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClickSeeProfile = () => {
|
const handleClickSeeProfile = () => {
|
||||||
closeModal();
|
closeModal();
|
||||||
if (friendCode.length === 8) {
|
if (friendCode.length === 8) {
|
||||||
navigate(`/user/${friendCode}`);
|
navigate(`/profile/${friendCode}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -80,7 +80,7 @@ export const UserFriendModalList = ({
|
||||||
|
|
||||||
const handleClickFriend = (userId: string) => {
|
const handleClickFriend = (userId: string) => {
|
||||||
closeModal();
|
closeModal();
|
||||||
navigate(`/user/${userId}`);
|
navigate(`/profile/${userId}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUndoFriendship = (userId: string) => {
|
const handleUndoFriendship = (userId: string) => {
|
||||||
|
|
|
@ -270,12 +270,6 @@ export interface RealDebridUser {
|
||||||
expiration: string;
|
expiration: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserDetails {
|
|
||||||
id: string;
|
|
||||||
displayName: string;
|
|
||||||
profileImageUrl: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserFriend {
|
export interface UserFriend {
|
||||||
id: string;
|
id: string;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue