feat: profile redesign

This commit is contained in:
Chubby Granny Chaser 2024-09-13 01:02:01 +01:00
parent 8f0003298f
commit d9a7672113
No known key found for this signature in database
24 changed files with 268 additions and 97 deletions

View file

@ -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",

View file

@ -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(

View file

@ -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;

View file

@ -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;
} }
}; };

View file

@ -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`, {

View file

@ -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({

View file

@ -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";

View file

@ -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>

View file

@ -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;

View file

@ -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,

View file

@ -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>;

View file

@ -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>) => {

View file

@ -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);
}, },

View file

@ -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>

View file

@ -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`,
});

View file

@ -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>
);
}

View file

@ -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>
); );
} }

View file

@ -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}

View file

@ -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={{

View file

@ -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",
});

View file

@ -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>

View file

@ -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}`);
} }
}; };

View file

@ -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) => {

View file

@ -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;