diff --git a/src/main/events/profile/index.ts b/src/main/events/profile/index.ts index 1548249f..664d6ee2 100644 --- a/src/main/events/profile/index.ts +++ b/src/main/events/profile/index.ts @@ -1,4 +1,3 @@ import "./get-me"; import "./process-profile-image"; -import "./sync-friend-requests"; import "./update-profile"; diff --git a/src/main/events/profile/sync-friend-requests.ts b/src/main/events/profile/sync-friend-requests.ts deleted file mode 100644 index 478c337f..00000000 --- a/src/main/events/profile/sync-friend-requests.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { registerEvent } from "../register-event"; -import { HydraApi, WindowManager } from "@main/services"; -import { UserNotLoggedInError } from "@shared"; -import type { FriendRequestSync } from "@types"; - -export const syncFriendRequests = async () => { - return HydraApi.get(`/profile/friend-requests/sync`) - .then((res) => { - WindowManager.mainWindow?.webContents.send( - "on-sync-friend-requests", - res - ); - - return res; - }) - .catch((err) => { - if (err instanceof UserNotLoggedInError) { - return { friendRequestCount: 0 } as FriendRequestSync; - } - throw err; - }); -}; - -registerEvent("syncFriendRequests", syncFriendRequests); diff --git a/src/preload/index.ts b/src/preload/index.ts index 7ad88a9b..32bc0f88 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -498,7 +498,6 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("updateProfile", updateProfile), processProfileImage: (imagePath: string) => ipcRenderer.invoke("processProfileImage", imagePath), - syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"), onSyncFriendRequests: (cb: (friendRequests: FriendRequestSync) => void) => { const listener = ( _event: Electron.IpcRendererEvent, diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 6619c890..7c0ea607 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -23,7 +23,6 @@ import { clearExtraction, } from "@renderer/features"; import { useTranslation } from "react-i18next"; -import { UserFriendModal } from "./pages/shared-modals/user-friend-modal"; import { useSubscription } from "./hooks/use-subscription"; import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal"; import { ArchiveDeletionModal } from "./pages/downloads/archive-deletion-error-modal"; @@ -56,10 +55,6 @@ export function App() { const { userDetails, hasActiveSubscription, - isFriendsModalVisible, - friendRequetsModalTab, - friendModalUserId, - hideFriendsModal, fetchUserDetails, updateUserDetails, clearUserDetails, @@ -135,7 +130,6 @@ export function App() { .then((response) => { if (response) { updateUserDetails(response); - window.electron.syncFriendRequests(); } }) .finally(() => { @@ -152,7 +146,6 @@ export function App() { fetchUserDetails().then((response) => { if (response) { updateUserDetails(response); - window.electron.syncFriendRequests(); showSuccessToast(t("successfully_signed_in")); } }); @@ -305,15 +298,6 @@ export function App() { onClose={() => setShowArchiveDeletionModal(false)} /> - {userDetails && ( - - )} -
diff --git a/src/renderer/src/components/sidebar/sidebar-profile.scss b/src/renderer/src/components/sidebar/sidebar-profile.scss index 4a061e98..8ec442f2 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.scss +++ b/src/renderer/src/components/sidebar/sidebar-profile.scss @@ -46,8 +46,7 @@ white-space: nowrap; } - &__notification-button, - &__friends-button { + &__notification-button { color: globals.$muted-color; cursor: pointer; border-radius: 50%; @@ -63,8 +62,7 @@ } } - &__notification-button-badge, - &__friends-button-badge { + &__notification-button-badge { background-color: globals.$success-color; display: flex; justify-content: center; diff --git a/src/renderer/src/components/sidebar/sidebar-profile.tsx b/src/renderer/src/components/sidebar/sidebar-profile.tsx index 2bc28d88..bd1209ec 100644 --- a/src/renderer/src/components/sidebar/sidebar-profile.tsx +++ b/src/renderer/src/components/sidebar/sidebar-profile.tsx @@ -1,9 +1,8 @@ import { useNavigate } from "react-router-dom"; -import { PeopleIcon, BellIcon } from "@primer/octicons-react"; +import { BellIcon } from "@primer/octicons-react"; import { useAppSelector, useUserDetails } from "@renderer/hooks"; import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import { Avatar } from "../avatar/avatar"; import { AuthPage } from "@shared"; @@ -16,8 +15,7 @@ export function SidebarProfile() { const { t } = useTranslation("sidebar"); - const { userDetails, friendRequestCount, showFriendsModal } = - useUserDetails(); + const { userDetails } = useUserDetails(); const { gameRunning } = useAppSelector((state) => state.gameRunning); @@ -114,29 +112,6 @@ export function SidebarProfile() { ); }, [t, notificationCount, navigate]); - const friendsButton = useMemo(() => { - if (!userDetails) return null; - - return ( - - ); - }, [userDetails, t, friendRequestCount, showFriendsModal]); - const gameRunningDetails = () => { if (!userDetails || !gameRunning) return null; @@ -185,7 +160,6 @@ export function SidebarProfile() { {notificationsButton} - {friendsButton} ); } diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 4bc3dabc..4e7fd245 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -389,7 +389,6 @@ declare global { processProfileImage: ( path: string ) => Promise<{ imagePath: string; mimeType: string }>; - syncFriendRequests: () => Promise; onSyncFriendRequests: ( cb: (friendRequests: FriendRequestSync) => void ) => () => Electron.IpcRenderer; diff --git a/src/renderer/src/features/user-details-slice.ts b/src/renderer/src/features/user-details-slice.ts index 8994f180..0f477ec2 100644 --- a/src/renderer/src/features/user-details-slice.ts +++ b/src/renderer/src/features/user-details-slice.ts @@ -1,5 +1,4 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit"; -import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; import type { FriendRequest, UserDetails } from "@types"; export interface UserDetailsState { @@ -7,9 +6,6 @@ export interface UserDetailsState { profileBackground: null | string; friendRequests: FriendRequest[]; friendRequestCount: number; - isFriendsModalVisible: boolean; - friendRequetsModalTab: UserFriendModalTab | null; - friendModalUserId: string; } const initialState: UserDetailsState = { @@ -17,9 +13,6 @@ const initialState: UserDetailsState = { profileBackground: null, friendRequests: [], friendRequestCount: 0, - isFriendsModalVisible: false, - friendRequetsModalTab: null, - friendModalUserId: "", }; export const userDetailsSlice = createSlice({ @@ -38,18 +31,6 @@ export const userDetailsSlice = createSlice({ setFriendRequestCount: (state, action: PayloadAction) => { state.friendRequestCount = action.payload; }, - setFriendsModalVisible: ( - state, - action: PayloadAction<{ initialTab: UserFriendModalTab; userId: string }> - ) => { - state.isFriendsModalVisible = true; - state.friendRequetsModalTab = action.payload.initialTab; - state.friendModalUserId = action.payload.userId; - }, - setFriendsModalHidden: (state) => { - state.isFriendsModalVisible = false; - state.friendRequetsModalTab = null; - }, }, }); @@ -58,6 +39,4 @@ export const { setProfileBackground, setFriendRequests, setFriendRequestCount, - setFriendsModalVisible, - setFriendsModalHidden, } = userDetailsSlice.actions; diff --git a/src/renderer/src/hooks/use-user-details.ts b/src/renderer/src/hooks/use-user-details.ts index 6d89f9b4..d8b9bbd2 100644 --- a/src/renderer/src/hooks/use-user-details.ts +++ b/src/renderer/src/hooks/use-user-details.ts @@ -4,8 +4,6 @@ import { setProfileBackground, setUserDetails, setFriendRequests, - setFriendsModalVisible, - setFriendsModalHidden, } from "@renderer/features"; import type { FriendRequestAction, @@ -13,20 +11,12 @@ import type { UserDetails, FriendRequest, } from "@types"; -import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal"; export function useUserDetails() { const dispatch = useAppDispatch(); - const { - userDetails, - profileBackground, - friendRequests, - friendRequestCount, - isFriendsModalVisible, - friendModalUserId, - friendRequetsModalTab, - } = useAppSelector((state) => state.userDetails); + const { userDetails, profileBackground, friendRequests, friendRequestCount } = + useAppSelector((state) => state.userDetails); const clearUserDetails = useCallback(async () => { dispatch(setUserDetails(null)); @@ -85,24 +75,11 @@ export function useUserDetails() { return window.electron.hydraApi .get("/profile/friend-requests") .then((friendRequests) => { - window.electron.syncFriendRequests(); dispatch(setFriendRequests(friendRequests)); }) .catch(() => {}); }, [dispatch]); - const showFriendsModal = useCallback( - (initialTab: UserFriendModalTab, userId: string) => { - dispatch(setFriendsModalVisible({ initialTab, userId })); - fetchFriendRequests(); - }, - [dispatch, fetchFriendRequests] - ); - - const hideFriendsModal = useCallback(() => { - dispatch(setFriendsModalHidden()); - }, [dispatch]); - const sendFriendRequest = useCallback( async (userId: string) => { return window.electron.hydraApi @@ -152,12 +129,7 @@ export function useUserDetails() { profileBackground, friendRequests, friendRequestCount, - friendRequetsModalTab, - isFriendsModalVisible, - friendModalUserId, hasActiveSubscription, - showFriendsModal, - hideFriendsModal, fetchUserDetails, signOut, clearUserDetails, diff --git a/src/renderer/src/pages/notifications/notifications.tsx b/src/renderer/src/pages/notifications/notifications.tsx index bceaff21..f9bd0b46 100644 --- a/src/renderer/src/pages/notifications/notifications.tsx +++ b/src/renderer/src/pages/notifications/notifications.tsx @@ -343,13 +343,17 @@ export default function Notifications() { ); }; - return ( - <> - {isLoading && mergedNotifications.length === 0 ? ( + const renderContent = () => { + if (isLoading && mergedNotifications.length === 0) { + return (
{t("loading")}
- ) : mergedNotifications.length === 0 ? ( + ); + } + + if (mergedNotifications.length === 0) { + return (
@@ -357,36 +361,40 @@ export default function Notifications() {

{t("empty_title")}

{t("empty_description")}

- ) : ( -
-
- - -
+ ); + } -
- - {displayedNotifications.map(renderNotification)} - -
- - {pagination.hasMore && ( -
- -
- )} + return ( +
+
+ +
- )} - - ); + +
+ + {displayedNotifications.map(renderNotification)} + +
+ + {pagination.hasMore && ( +
+ +
+ )} +
+ ); + }; + + return <>{renderContent()}; } diff --git a/src/renderer/src/pages/profile/profile-content/add-friend-modal.scss b/src/renderer/src/pages/profile/profile-content/add-friend-modal.scss index a2db81ef..f33b5515 100644 --- a/src/renderer/src/pages/profile/profile-content/add-friend-modal.scss +++ b/src/renderer/src/pages/profile/profile-content/add-friend-modal.scss @@ -51,4 +51,29 @@ overflow-y: auto; padding-right: globals.$spacing-unit; } + + &__friend-item { + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit * 2); + padding: calc(globals.$spacing-unit * 1.5); + background-color: rgba(255, 255, 255, 0.05); + border-radius: 8px; + cursor: pointer; + transition: all ease 0.2s; + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + } + + &__friend-name { + flex: 1; + font-weight: 600; + color: globals.$muted-color; + font-size: globals.$body-font-size; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } } diff --git a/src/renderer/src/pages/profile/profile-content/add-friend-modal.tsx b/src/renderer/src/pages/profile/profile-content/add-friend-modal.tsx index 04838f6f..efe95ec5 100644 --- a/src/renderer/src/pages/profile/profile-content/add-friend-modal.tsx +++ b/src/renderer/src/pages/profile/profile-content/add-friend-modal.tsx @@ -1,9 +1,8 @@ -import { Button, Modal, TextField } from "@renderer/components"; +import { Avatar, Button, Modal, TextField } from "@renderer/components"; import { useToast, useUserDetails } from "@renderer/hooks"; import { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { UserFriendItem } from "@renderer/pages/shared-modals/user-friend-modal/user-friend-item"; import "./add-friend-modal.scss"; interface AddFriendModalProps { @@ -80,22 +79,6 @@ export function AddFriendModal({ visible, onClose }: AddFriendModalProps) { }); }; - const handleAcceptFriendRequest = (userId: string) => { - updateFriendRequestState(userId, "ACCEPTED") - .then(() => { - showSuccessToast(t("request_accepted")); - }) - .catch(() => { - showErrorToast(t("try_again")); - }); - }; - - const handleRefuseFriendRequest = (userId: string) => { - updateFriendRequestState(userId, "REFUSED").catch(() => { - showErrorToast(t("try_again")); - }); - }; - const sentRequests = friendRequests.filter((req) => req.type === "SENT"); const currentRequest = friendCode.length === 8 @@ -139,17 +122,31 @@ export function AddFriendModal({ visible, onClose }: AddFriendModalProps) {

{t("pending")}

{sentRequests.map((request) => ( - + type="button" + className="add-friend-modal__friend-item" + onClick={() => handleClickRequest(request.id)} + > + + + {request.displayName} + + + ))}
diff --git a/src/renderer/src/pages/profile/profile-content/badges-box.scss b/src/renderer/src/pages/profile/profile-content/badges-box.scss index 1c53050d..ce8622ac 100644 --- a/src/renderer/src/pages/profile/profile-content/badges-box.scss +++ b/src/renderer/src/pages/profile/profile-content/badges-box.scss @@ -1,18 +1,14 @@ @use "../../../scss/globals.scss"; .badges-box { - &__section-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: calc(globals.$spacing-unit * 2); + &__box { + padding: calc(globals.$spacing-unit * 2); } - &__box { - background-color: globals.$background-color; - border-radius: 4px; - border: solid 1px globals.$border-color; - padding: calc(globals.$spacing-unit * 2); + &__header { + display: flex; + justify-content: flex-end; + margin-bottom: calc(globals.$spacing-unit * 2); } &__list { @@ -24,7 +20,7 @@ &__item { display: flex; align-items: center; - gap: calc(globals.$spacing-unit * 2); + gap: calc(globals.$spacing-unit * 1.5); width: 100%; padding: calc(globals.$spacing-unit * 1.5); background-color: rgba(255, 255, 255, 0.05); @@ -38,8 +34,8 @@ &__item-icon { flex-shrink: 0; - width: 48px; - height: 48px; + width: 34px; + height: 34px; border-radius: 8px; overflow: hidden; display: flex; @@ -48,8 +44,8 @@ background-color: globals.$background-color; img { - width: 32px; - height: 32px; + width: 28px; + height: 28px; object-fit: contain; } } @@ -63,7 +59,7 @@ } &__item-title { - font-size: globals.$body-font-size; + font-size: 0.8rem; font-weight: 600; color: globals.$body-color; margin: 0; @@ -76,7 +72,6 @@ } &__view-all-container { - background-color: globals.$background-color; padding-top: calc(globals.$spacing-unit * 2); margin-top: calc(globals.$spacing-unit * 2); display: flex; diff --git a/src/renderer/src/pages/profile/profile-content/badges-box.tsx b/src/renderer/src/pages/profile/profile-content/badges-box.tsx index b898be2a..501341b2 100644 --- a/src/renderer/src/pages/profile/profile-content/badges-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/badges-box.tsx @@ -1,5 +1,4 @@ import { userProfileContext } from "@renderer/context"; -import { useFormat } from "@renderer/hooks"; import { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { AllBadgesModal } from "./all-badges-modal"; @@ -10,7 +9,6 @@ const MAX_VISIBLE_BADGES = 4; export function BadgesBox() { const { userProfile, badges } = useContext(userProfileContext); const { t } = useTranslation("user_profile"); - const { numberFormatter } = useFormat(); const [showAllBadgesModal, setShowAllBadgesModal] = useState(false); if (!userProfile?.badges.length) return null; @@ -20,55 +18,44 @@ export function BadgesBox() { return ( <> -
-
-
-

{t("badges")}

- - {numberFormatter.format(userProfile.badges.length)} - -
-
+
+
+ {visibleBadges.map((badgeName) => { + const badge = badges.find((b) => b.name === badgeName); -
-
- {visibleBadges.map((badgeName) => { - const badge = badges.find((b) => b.name === badgeName); + if (!badge) return null; - if (!badge) return null; - - return ( -
-
- {badge.name} -
-
-

{badge.title}

-

- {badge.description} -

-
+ return ( +
+
+ {badge.name}
- ); - })} -
- {hasMoreBadges && ( -
- -
- )} +
+

{badge.title}

+

+ {badge.description} +

+
+
+ ); + })}
+ {hasMoreBadges && ( +
+ +
+ )}
-
-
-
-

{t("friends")}

- {userStats && ( - - {numberFormatter.format(userStats.friendsCount)} - - )} -
- {isMe && ( - - )} -
- -
-
    - {userProfile?.friends.map((friend) => ( -
  • - - + -
    - - {friend.displayName} - - {friend.currentGame && ( -
    - {getGameImage(friend.currentGame)} - {friend.currentGame.title} -
    - )} -
    - -
  • - ))} -
-
- -
+
+ + {friend.displayName} + + {friend.currentGame && ( +
+ {getGameImage(friend.currentGame)} + {friend.currentGame.title} +
+ )} +
+ + + ))} + +
+
@@ -125,3 +101,31 @@ export function FriendsBox() { ); } + +export function FriendsBoxAddButton() { + const { userProfile } = useContext(userProfileContext); + const { userDetails } = useUserDetails(); + const { t } = useTranslation("user_profile"); + const [showAddFriendModal, setShowAddFriendModal] = useState(false); + + const isMe = userDetails?.id === userProfile?.id; + + if (!isMe) return null; + + return ( + <> + + setShowAddFriendModal(false)} + /> + + ); +} diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index c425b047..8c200b06 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -15,10 +15,11 @@ import type { GameShop } from "@types"; import { LockedProfile } from "./locked-profile"; import { ReportProfile } from "../report-profile/report-profile"; import { BadgesBox } from "./badges-box"; -import { FriendsBox } from "./friends-box"; +import { FriendsBox, FriendsBoxAddButton } from "./friends-box"; import { RecentGamesBox } from "./recent-games-box"; import { UserStatsBox } from "./user-stats-box"; import { UserKarmaBox } from "./user-karma-box"; +import { ProfileSection } from "../profile-section/profile-section"; import { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal"; import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations"; import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants"; @@ -187,8 +188,6 @@ export function ProfileContent() { ); setReviews(response.reviews); setReviewsTotalCount(response.totalCount); - } catch (error) { - // Error handling for fetching reviews } finally { setIsLoadingReviews(false); } @@ -427,11 +426,41 @@ export function ProfileContent() { {shouldShowRightContent && (
- - - - - + {userStats && ( + + + + )} + {userProfile?.badges.length > 0 && ( + + + + )} + {userProfile?.karma !== undefined && + userProfile?.karma !== null && ( + + + + )} + {userProfile?.recentGames.length > 0 && ( + + + + )} + {userProfile?.friends.length > 0 && ( + } + defaultOpen={true} + > + + + )}
)} diff --git a/src/renderer/src/pages/profile/profile-content/recent-games-box.scss b/src/renderer/src/pages/profile/profile-content/recent-games-box.scss index 6478fd79..4bfb51f0 100644 --- a/src/renderer/src/pages/profile/profile-content/recent-games-box.scss +++ b/src/renderer/src/pages/profile/profile-content/recent-games-box.scss @@ -2,19 +2,9 @@ .recent-games { &__box { - background-color: globals.$background-color; - border-radius: 4px; - border: solid 1px globals.$border-color; padding: calc(globals.$spacing-unit * 2); } - &__section-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: calc(globals.$spacing-unit * 2); - } - &__list { list-style: none; margin: 0; diff --git a/src/renderer/src/pages/profile/profile-content/recent-games-box.tsx b/src/renderer/src/pages/profile/profile-content/recent-games-box.tsx index 5e13b0a9..e61ca423 100644 --- a/src/renderer/src/pages/profile/profile-content/recent-games-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/recent-games-box.tsx @@ -42,38 +42,32 @@ export function RecentGamesBox() { if (!userProfile?.recentGames.length) return null; return ( -
-
-

{t("activity")}

-
+
+
    + {userProfile?.recentGames.map((game) => ( +
  • + + {game.title} -
    -
      - {userProfile?.recentGames.map((game) => ( -
    • - - {game.title} +
      + {game.title} -
      - {game.title} - -
      - - {formatPlayTime(game)} -
      +
      + + {formatPlayTime(game)}
      - -
    • - ))} -
    -
    +
+ + + ))} +
); } diff --git a/src/renderer/src/pages/profile/profile-content/user-karma-box.scss b/src/renderer/src/pages/profile/profile-content/user-karma-box.scss index 63015b4d..79261aa1 100644 --- a/src/renderer/src/pages/profile/profile-content/user-karma-box.scss +++ b/src/renderer/src/pages/profile/profile-content/user-karma-box.scss @@ -2,19 +2,9 @@ .user-karma { &__box { - background-color: globals.$background-color; - border-radius: 4px; - border: solid 1px globals.$border-color; padding: calc(globals.$spacing-unit * 2); } - &__section-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: calc(globals.$spacing-unit * 2); - } - &__content { display: flex; flex-direction: column; diff --git a/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx b/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx index d2232276..b8dd7244 100644 --- a/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-karma-box.tsx @@ -18,24 +18,18 @@ export function UserKarmaBox() { if (karma === undefined || karma === null) return null; return ( -
-
-

{t("karma")}

-
- -
-
-
-

- {numberFormatter.format(karma)}{" "} - {t("karma_count")} -

-
-
- - {t("karma_description")} - -
+
+
+
+

+ {numberFormatter.format(karma)}{" "} + {t("karma_count")} +

+
+
+ + {t("karma_description")} +
diff --git a/src/renderer/src/pages/profile/profile-content/user-stats-box.scss b/src/renderer/src/pages/profile/profile-content/user-stats-box.scss index c19fb612..fc92e466 100644 --- a/src/renderer/src/pages/profile/profile-content/user-stats-box.scss +++ b/src/renderer/src/pages/profile/profile-content/user-stats-box.scss @@ -2,19 +2,9 @@ .user-stats { &__box { - background-color: globals.$background-color; - border-radius: 4px; - border: solid 1px globals.$border-color; padding: calc(globals.$spacing-unit * 2); } - &__section-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: calc(globals.$spacing-unit * 2); - } - &__list { list-style: none; margin: 0; diff --git a/src/renderer/src/pages/profile/profile-content/user-stats-box.tsx b/src/renderer/src/pages/profile/profile-content/user-stats-box.tsx index 26ec79f4..f7e76e79 100644 --- a/src/renderer/src/pages/profile/profile-content/user-stats-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/user-stats-box.tsx @@ -34,87 +34,81 @@ export function UserStatsBox() { if (!userStats) return null; return ( -
-
-

{t("stats")}

-
- -
-
    - {(isMe || userStats.unlockedAchievementSum !== undefined) && ( -
  • -

    - {t("achievements_unlocked")} -

    - {userStats.unlockedAchievementSum !== undefined ? ( -
    -

    - {userStats.unlockedAchievementSum}{" "} - {t("achievements")} -

    -
    - ) : ( - - )} -
  • - )} - - {(isMe || userStats.achievementsPointsEarnedSum !== undefined) && ( -
  • -

    {t("earned_points")}

    - {userStats.achievementsPointsEarnedSum !== undefined ? ( -
    -

    - - {numberFormatter.format( - userStats.achievementsPointsEarnedSum.value - )} -

    -

    - {t("top_percentile", { - percentile: - userStats.achievementsPointsEarnedSum.topPercentile, - })} -

    -
    - ) : ( - - )} -
  • - )} - +
    +
      + {(isMe || userStats.unlockedAchievementSum !== undefined) && (
    • -

      {t("total_play_time")}

      -
      -

      - - {formatPlayTime(userStats.totalPlayTimeInSeconds.value)} -

      -

      - {t("top_percentile", { - percentile: userStats.totalPlayTimeInSeconds.topPercentile, - })} -

      -
      +

      + {t("achievements_unlocked")} +

      + {userStats.unlockedAchievementSum !== undefined ? ( +
      +

      + {userStats.unlockedAchievementSum}{" "} + {t("achievements")} +

      +
      + ) : ( + + )}
    • -
    -
    + )} + + {(isMe || userStats.achievementsPointsEarnedSum !== undefined) && ( +
  • +

    {t("earned_points")}

    + {userStats.achievementsPointsEarnedSum !== undefined ? ( +
    +

    + + {numberFormatter.format( + userStats.achievementsPointsEarnedSum.value + )} +

    +

    + {t("top_percentile", { + percentile: + userStats.achievementsPointsEarnedSum.topPercentile, + })} +

    +
    + ) : ( + + )} +
  • + )} + +
  • +

    {t("total_play_time")}

    +
    +

    + + {formatPlayTime(userStats.totalPlayTimeInSeconds.value)} +

    +

    + {t("top_percentile", { + percentile: userStats.totalPlayTimeInSeconds.topPercentile, + })} +

    +
    +
  • +
); } diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.scss b/src/renderer/src/pages/profile/profile-hero/profile-hero.scss index 44abebc6..7f3b757a 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.scss +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.scss @@ -30,7 +30,7 @@ &__copy-button { display: flex; align-items: center; - justify-content: center; + justify-content: flex-start; background: none; border: none; color: globals.$body-color; @@ -38,7 +38,11 @@ cursor: pointer; padding: calc(globals.$spacing-unit / 1.5); border-radius: 6px; - transition: all ease 0.2s; + transition: background-color ease 0.2s; + overflow: hidden; + white-space: nowrap; + flex-shrink: 0; + box-sizing: border-box; &:hover { opacity: 1; @@ -46,6 +50,12 @@ } } + &__friend-code { + font-size: globals.$small-font-size; + font-family: monospace; + white-space: nowrap; + } + &__user-information { display: flex; padding: calc(globals.$spacing-unit * 7) calc(globals.$spacing-unit * 3); diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx index 9755fb68..7c4c1e75 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.tsx @@ -20,6 +20,7 @@ import { } from "@renderer/hooks"; import { addSeconds } from "date-fns"; import { useNavigate } from "react-router-dom"; +import { motion } from "framer-motion"; import type { FriendRequestAction } from "@types"; import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal"; @@ -34,6 +35,7 @@ type FriendAction = export function ProfileHero() { const [showEditProfileModal, setShowEditProfileModal] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false); + const [isCopyButtonHovered, setIsCopyButtonHovered] = useState(false); const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } = useContext(userProfileContext); @@ -312,14 +314,32 @@ export function ProfileHero() { {userProfile?.displayName} - + + {userProfile?.id} + +
) : ( diff --git a/src/renderer/src/pages/profile/profile-section/profile-section.scss b/src/renderer/src/pages/profile/profile-section/profile-section.scss new file mode 100644 index 00000000..dfc6abf9 --- /dev/null +++ b/src/renderer/src/pages/profile/profile-section/profile-section.scss @@ -0,0 +1,71 @@ +@use "../../../scss/globals.scss"; + +.profile-section { + background-color: globals.$background-color; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.05); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + overflow: hidden; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + background-color: globals.$background-color; + padding-right: calc(globals.$spacing-unit * 2); + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + } + } + + &__button { + padding: calc(globals.$spacing-unit * 2.5) calc(globals.$spacing-unit * 2); + display: flex; + align-items: center; + background-color: transparent; + color: globals.$muted-color; + flex: 1; + cursor: pointer; + transition: all ease 0.2s; + gap: globals.$spacing-unit; + font-size: globals.$body-font-size; + font-weight: bold; + border: none; + + &:active { + opacity: globals.$active-opacity; + } + } + + &__count { + background-color: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + min-width: 24px; + text-align: center; + } + + &__action { + display: flex; + align-items: center; + } + + &__chevron { + transition: transform ease 0.2s; + + &--open { + transform: rotate(180deg); + } + } + + &__content { + overflow: hidden; + transition: max-height 0.4s cubic-bezier(0, 1, 0, 1); + background-color: globals.$dark-background-color; + position: relative; + } +} diff --git a/src/renderer/src/pages/profile/profile-section/profile-section.tsx b/src/renderer/src/pages/profile/profile-section/profile-section.tsx new file mode 100644 index 00000000..4586d58f --- /dev/null +++ b/src/renderer/src/pages/profile/profile-section/profile-section.tsx @@ -0,0 +1,64 @@ +import { ChevronDownIcon } from "@primer/octicons-react"; +import { useEffect, useRef, useState } from "react"; +import "./profile-section.scss"; + +export interface ProfileSectionProps { + title: string; + count?: number; + action?: React.ReactNode; + children: React.ReactNode; + defaultOpen?: boolean; +} + +export function ProfileSection({ + title, + count, + action, + children, + defaultOpen = true, +}: ProfileSectionProps) { + const content = useRef(null); + const [isOpen, setIsOpen] = useState(defaultOpen); + const [height, setHeight] = useState(0); + + useEffect(() => { + if (content.current && content.current.scrollHeight !== height) { + setHeight(isOpen ? content.current.scrollHeight : 0); + } else if (!isOpen) { + setHeight(0); + } + }, [isOpen, children, height]); + + return ( +
+
+ + {action &&
{action}
} +
+ +
+ {children} +
+
+ ); +} diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/index.ts b/src/renderer/src/pages/shared-modals/user-friend-modal/index.ts deleted file mode 100644 index c7484512..00000000 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./user-friend-modal"; diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.scss b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.scss deleted file mode 100644 index 216538ff..00000000 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.scss +++ /dev/null @@ -1,79 +0,0 @@ -@use "../../../scss/globals.scss"; - -.user-friend-item { - &__container { - display: flex; - gap: calc(globals.$spacing-unit * 3); - align-items: center; - border-radius: 4px; - border: solid 1px globals.$border-color; - width: 100%; - height: 54px; - min-height: 54px; - transition: all ease 0.2s; - position: relative; - - &:hover { - background-color: rgba(255, 255, 255, 0.15); - } - } - - &__button { - display: flex; - align-items: center; - position: absolute; - cursor: pointer; - height: 100%; - width: 100%; - flex-direction: row; - color: globals.$body-color; - gap: calc(globals.$spacing-unit + globals.$spacing-unit / 2); - padding: 0 globals.$spacing-unit; - - &__content { - display: flex; - flex-direction: column; - align-items: flex-start; - flex: 1; - min-width: 0; - } - - &__actions { - position: absolute; - right: 8px; - display: flex; - gap: 8px; - } - } - - &__display-name { - font-weight: bold; - font-size: globals.$body-font-size; - text-align: left; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__accept-button { - cursor: pointer; - color: globals.$body-color; - width: 28px; - height: 28px; - - &:hover { - color: globals.$success-color; - } - } - - &__cancel-button { - cursor: pointer; - color: globals.$body-color; - width: 28px; - height: 28px; - - &:hover { - color: globals.$danger-color; - } - } -} diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx deleted file mode 100644 index 0538e717..00000000 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-item.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { CheckCircleIcon, XCircleIcon } from "@primer/octicons-react"; -import { useTranslation } from "react-i18next"; -import { Avatar } from "@renderer/components"; -import "./user-friend-item.scss"; - -export type UserFriendItemProps = { - userId: string; - profileImageUrl: string | null; - displayName: string; -} & ( - | { - type: "ACCEPTED"; - onClickUndoFriendship: (userId: string) => void; - onClickItem: (userId: string) => void; - } - | { type: "BLOCKED"; onClickUnblock: (userId: string) => void } - | { - type: "SENT" | "RECEIVED"; - onClickCancelRequest: (userId: string) => void; - onClickAcceptRequest: (userId: string) => void; - onClickRefuseRequest: (userId: string) => void; - onClickItem: (userId: string) => void; - } - | { type: null; onClickItem: (userId: string) => void } -); - -export const UserFriendItem = (props: UserFriendItemProps) => { - const { t } = useTranslation("user_profile"); - const { userId, profileImageUrl, displayName, type } = props; - - const getRequestDescription = () => { - if (type === "ACCEPTED" || type === null) return null; - - return ( - - {type == "SENT" ? t("request_sent") : t("request_received")} - - ); - }; - - const getRequestActions = () => { - if (type === null) return null; - - if (type === "SENT") { - return ( - - ); - } - - if (type === "RECEIVED") { - return ( - <> - - - - ); - } - - if (type === "ACCEPTED") { - return ( - - ); - } - - if (type === "BLOCKED") { - return ( - - ); - } - - return null; - }; - - if (type === "BLOCKED") { - return ( -
-
- -
-

{displayName}

-
-
-
- {getRequestActions()} -
-
- ); - } - - return ( -
- -
- {getRequestActions()} -
-
- ); -}; diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.scss b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.scss deleted file mode 100644 index 8c896a9e..00000000 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.scss +++ /dev/null @@ -1,21 +0,0 @@ -@use "../../../scss/globals.scss"; - -.user-friend-modal-add-friend { - &__actions { - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - gap: globals.$spacing-unit; - } - - &__button { - align-self: end; - } - - &__pending-container { - display: flex; - flex-direction: column; - gap: calc(globals.$spacing-unit * 2); - } -} diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx deleted file mode 100644 index 84248522..00000000 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-add-friend.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { Button, TextField } from "@renderer/components"; -import { useToast, useUserDetails } from "@renderer/hooks"; -import { useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import { UserFriendItem } from "./user-friend-item"; -import "./user-friend-modal-add-friend.scss"; - -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 { showSuccessToast, showErrorToast } = useToast(); - - const handleClickAddFriend = () => { - setIsAddingFriend(true); - sendFriendRequest(friendCode) - .then(() => { - setFriendCode(""); - }) - .catch(() => { - showErrorToast(t("error_adding_friend")); - }) - .finally(() => { - setIsAddingFriend(false); - }); - }; - - const handleClickRequest = (userId: string) => { - closeModal(); - navigate(`/profile/${userId}`); - }; - - const handleClickSeeProfile = () => { - if (friendCode.length === 8) { - closeModal(); - navigate(`/profile/${friendCode}`); - } - }; - - const validateFriendCode = (callback: () => void) => { - if (friendCode.length === 8) { - return callback(); - } - - showErrorToast(t("friend_code_length_error")); - }; - - const handleCancelFriendRequest = (userId: string) => { - updateFriendRequestState(userId, "CANCEL").catch(() => { - showErrorToast(t("try_again")); - }); - }; - - const handleAcceptFriendRequest = (userId: string) => { - updateFriendRequestState(userId, "ACCEPTED") - .then(() => { - showSuccessToast(t("request_accepted")); - }) - .catch(() => { - showErrorToast(t("try_again")); - }); - }; - - const handleRefuseFriendRequest = (userId: string) => { - updateFriendRequestState(userId, "REFUSED").catch(() => { - showErrorToast(t("try_again")); - }); - }; - - const handleChangeFriendCode = (e: React.ChangeEvent) => { - const friendCode = e.target.value.trim().slice(0, 8); - setFriendCode(friendCode); - }; - - return ( - <> -
- - - - -
- -
-

{t("pending")}

- {friendRequests.length === 0 &&

{t("no_pending_invites")}

} - {friendRequests.map((request) => { - return ( - - ); - })} -
- - ); -}; diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.scss b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.scss deleted file mode 100644 index eaf0e527..00000000 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.scss +++ /dev/null @@ -1,16 +0,0 @@ -@use "../../../scss/globals.scss"; - -.user-friend-modal-list { - display: flex; - flex-direction: column; - gap: calc(globals.$spacing-unit * 2); - max-height: 400px; - overflow-y: scroll; - - &__skeleton { - width: 100%; - height: 54px; - overflow: hidden; - border-radius: 4px; - } -} diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx deleted file mode 100644 index d5259777..00000000 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import type { UserFriend } from "@types"; -import { useEffect, useRef, useState } from "react"; -import { UserFriendItem } from "./user-friend-item"; -import { useNavigate } from "react-router-dom"; -import { useUserDetails } from "@renderer/hooks"; -import { useTranslation } from "react-i18next"; -import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; -import "./user-friend-modal-list.scss"; - -export interface UserFriendModalListProps { - userId: string; - closeModal: () => void; -} - -const pageSize = 12; - -export const UserFriendModalList = ({ - userId, - closeModal, -}: UserFriendModalListProps) => { - const { t } = useTranslation("user_profile"); - const navigate = useNavigate(); - - const [page, setPage] = useState(0); - const [isLoading, setIsLoading] = useState(false); - const [maxPage, setMaxPage] = useState(0); - const [friends, setFriends] = useState([]); - const listContainer = useRef(null); - - const { userDetails } = useUserDetails(); - const isMe = userDetails?.id == userId; - - const loadNextPage = () => { - if (page > maxPage) return; - setIsLoading(true); - - const url = isMe ? "/profile/friends" : `/users/${userId}/friends`; - - window.electron.hydraApi - .get<{ totalFriends: number; friends: UserFriend[] }>(url, { - params: { take: pageSize, skip: page * pageSize }, - }) - .then((newPage) => { - if (page === 0) { - setMaxPage(newPage.totalFriends / pageSize); - } - - setFriends([...friends, ...newPage.friends]); - setPage(page + 1); - }) - .catch(() => {}) - .finally(() => setIsLoading(false)); - }; - - const handleScroll = () => { - const scrollTop = listContainer.current?.scrollTop || 0; - const scrollHeight = listContainer.current?.scrollHeight || 0; - const clientHeight = listContainer.current?.clientHeight || 0; - const maxScrollTop = scrollHeight - clientHeight; - - if (scrollTop < maxScrollTop * 0.9 || isLoading) { - return; - } - - loadNextPage(); - }; - - useEffect(() => { - const container = listContainer.current; - container?.addEventListener("scroll", handleScroll); - return () => container?.removeEventListener("scroll", handleScroll); - }, [isLoading]); - - const reloadList = () => { - setPage(0); - setMaxPage(0); - setFriends([]); - loadNextPage(); - }; - - useEffect(() => { - reloadList(); - }, [userId]); - - const handleClickFriend = (userId: string) => { - closeModal(); - navigate(`/profile/${userId}`); - }; - - return ( - -
- {!isLoading && friends.length === 0 &&

{t("no_friends_added")}

} - {friends.map((friend) => ( - - ))} - {isLoading && } -
-
- ); -}; diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.scss b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.scss deleted file mode 100644 index 550c0fd9..00000000 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.scss +++ /dev/null @@ -1,34 +0,0 @@ -@use "../../../scss/globals.scss"; - -.user-friend-modal { - &__container { - display: flex; - width: 500px; - flex-direction: column; - gap: calc(globals.$spacing-unit * 2); - } - - &__header { - display: flex; - gap: globals.$spacing-unit; - align-items: center; - } - - &__friend-code-button { - color: globals.$body-color; - cursor: pointer; - display: flex; - gap: calc(globals.$spacing-unit / 2); - align-items: center; - transition: all ease 0.2s; - - &:hover { - color: globals.$muted-color; - } - } - - &__tabs { - display: flex; - gap: globals.$spacing-unit; - } -} diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.tsx deleted file mode 100644 index 7c045394..00000000 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { Button, Modal } from "@renderer/components"; -import { useCallback, useEffect, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { UserFriendModalAddFriend } from "./user-friend-modal-add-friend"; -import { useToast, useUserDetails } from "@renderer/hooks"; -import { UserFriendModalList } from "./user-friend-modal-list"; -import { CopyIcon } from "@primer/octicons-react"; -import "./user-friend-modal.scss"; - -export enum UserFriendModalTab { - FriendsList, - AddFriend, -} - -export interface UserFriendsModalProps { - visible: boolean; - onClose: () => void; - initialTab: UserFriendModalTab | null; - userId: string; -} - -export const UserFriendModal = ({ - visible, - onClose, - initialTab, - userId, -}: UserFriendsModalProps) => { - const { t } = useTranslation("user_profile"); - - const tabs = [t("friends_list"), t("add_friends")]; - - const [currentTab, setCurrentTab] = useState( - initialTab || UserFriendModalTab.FriendsList - ); - - const { showSuccessToast } = useToast(); - - const { userDetails } = useUserDetails(); - const isMe = userDetails?.id == userId; - - useEffect(() => { - if (initialTab != null) { - setCurrentTab(initialTab); - } - }, [initialTab]); - - const renderTab = () => { - if (currentTab == UserFriendModalTab.FriendsList) { - return ; - } - - if (currentTab == UserFriendModalTab.AddFriend) { - return ; - } - - return <>; - }; - - const copyToClipboard = useCallback(() => { - navigator.clipboard.writeText(userDetails!.id); - showSuccessToast(t("friend_code_copied")); - }, [userDetails, showSuccessToast, t]); - - return ( - -
- {isMe && ( - <> -
-

{t("your_friend_code")}

- -
-
- {tabs.map((tab, index) => ( - - ))} -
- - )} - {renderTab()} -
-
- ); -};