mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-11 13:56:16 +00:00
refactor: remove sync friend requests functionality and related components
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
import "./get-me";
|
||||
import "./process-profile-image";
|
||||
import "./sync-friend-requests";
|
||||
import "./update-profile";
|
||||
|
||||
@@ -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<FriendRequestSync>(`/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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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 && (
|
||||
<UserFriendModal
|
||||
visible={isFriendsModalVisible}
|
||||
initialTab={friendRequetsModalTab}
|
||||
onClose={hideFriendsModal}
|
||||
userId={friendModalUserId}
|
||||
/>
|
||||
)}
|
||||
|
||||
<main>
|
||||
<Sidebar />
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
type="button"
|
||||
className="sidebar-profile__friends-button"
|
||||
onClick={() =>
|
||||
showFriendsModal(UserFriendModalTab.AddFriend, userDetails.id)
|
||||
}
|
||||
title={t("friends")}
|
||||
>
|
||||
{friendRequestCount > 0 && (
|
||||
<small className="sidebar-profile__friends-button-badge">
|
||||
{friendRequestCount > 99 ? "99+" : friendRequestCount}
|
||||
</small>
|
||||
)}
|
||||
|
||||
<PeopleIcon size={16} />
|
||||
</button>
|
||||
);
|
||||
}, [userDetails, t, friendRequestCount, showFriendsModal]);
|
||||
|
||||
const gameRunningDetails = () => {
|
||||
if (!userDetails || !gameRunning) return null;
|
||||
|
||||
@@ -185,7 +160,6 @@ export function SidebarProfile() {
|
||||
</button>
|
||||
|
||||
{notificationsButton}
|
||||
{friendsButton}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
@@ -389,7 +389,6 @@ declare global {
|
||||
processProfileImage: (
|
||||
path: string
|
||||
) => Promise<{ imagePath: string; mimeType: string }>;
|
||||
syncFriendRequests: () => Promise<void>;
|
||||
onSyncFriendRequests: (
|
||||
cb: (friendRequests: FriendRequestSync) => void
|
||||
) => () => Electron.IpcRenderer;
|
||||
|
||||
@@ -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<number>) => {
|
||||
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;
|
||||
|
||||
@@ -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<FriendRequest[]>("/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,
|
||||
|
||||
@@ -343,13 +343,17 @@ export default function Notifications() {
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading && mergedNotifications.length === 0 ? (
|
||||
const renderContent = () => {
|
||||
if (isLoading && mergedNotifications.length === 0) {
|
||||
return (
|
||||
<div className="notifications__loading">
|
||||
<span>{t("loading")}</span>
|
||||
</div>
|
||||
) : mergedNotifications.length === 0 ? (
|
||||
);
|
||||
}
|
||||
|
||||
if (mergedNotifications.length === 0) {
|
||||
return (
|
||||
<div className="notifications__empty">
|
||||
<div className="notifications__icon-container">
|
||||
<BellIcon size={24} />
|
||||
@@ -357,36 +361,40 @@ export default function Notifications() {
|
||||
<h2>{t("empty_title")}</h2>
|
||||
<p>{t("empty_description")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="notifications">
|
||||
<div className="notifications__actions">
|
||||
<Button theme="outline" onClick={handleMarkAllAsRead}>
|
||||
{t("mark_all_as_read")}
|
||||
</Button>
|
||||
<Button theme="danger" onClick={handleClearAll}>
|
||||
{t("clear_all")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
<div className="notifications__list">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{displayedNotifications.map(renderNotification)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{pagination.hasMore && (
|
||||
<div className="notifications__load-more">
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={handleLoadMore}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? t("loading") : t("load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div className="notifications">
|
||||
<div className="notifications__actions">
|
||||
<Button theme="outline" onClick={handleMarkAllAsRead}>
|
||||
{t("mark_all_as_read")}
|
||||
</Button>
|
||||
<Button theme="danger" onClick={handleClearAll}>
|
||||
{t("clear_all")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
<div className="notifications__list">
|
||||
<AnimatePresence mode="popLayout">
|
||||
{displayedNotifications.map(renderNotification)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{pagination.hasMore && (
|
||||
<div className="notifications__load-more">
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={handleLoadMore}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? t("loading") : t("load_more")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return <>{renderContent()}</>;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
<h3>{t("pending")}</h3>
|
||||
<div className="add-friend-modal__pending-list">
|
||||
{sentRequests.map((request) => (
|
||||
<UserFriendItem
|
||||
<button
|
||||
key={request.id}
|
||||
displayName={request.displayName}
|
||||
type={request.type}
|
||||
profileImageUrl={request.profileImageUrl}
|
||||
userId={request.id}
|
||||
onClickAcceptRequest={handleAcceptFriendRequest}
|
||||
onClickCancelRequest={handleCancelFriendRequest}
|
||||
onClickRefuseRequest={handleRefuseFriendRequest}
|
||||
onClickItem={handleClickRequest}
|
||||
/>
|
||||
type="button"
|
||||
className="add-friend-modal__friend-item"
|
||||
onClick={() => handleClickRequest(request.id)}
|
||||
>
|
||||
<Avatar
|
||||
src={request.profileImageUrl}
|
||||
alt={request.displayName}
|
||||
size={40}
|
||||
/>
|
||||
<span className="add-friend-modal__friend-name">
|
||||
{request.displayName}
|
||||
</span>
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCancelFriendRequest(request.id);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{t("cancel_request")}
|
||||
</Button>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div>
|
||||
<div className="badges-box__section-header">
|
||||
<div className="profile-content__section-title-group">
|
||||
<h2>{t("badges")}</h2>
|
||||
<span className="profile-content__section-badge">
|
||||
{numberFormatter.format(userProfile.badges.length)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="badges-box__box">
|
||||
<div className="badges-box__list">
|
||||
{visibleBadges.map((badgeName) => {
|
||||
const badge = badges.find((b) => b.name === badgeName);
|
||||
|
||||
<div className="badges-box__box">
|
||||
<div className="badges-box__list">
|
||||
{visibleBadges.map((badgeName) => {
|
||||
const badge = badges.find((b) => b.name === badgeName);
|
||||
if (!badge) return null;
|
||||
|
||||
if (!badge) return null;
|
||||
|
||||
return (
|
||||
<div key={badge.name} className="badges-box__item">
|
||||
<div className="badges-box__item-icon">
|
||||
<img
|
||||
src={badge.badge.url}
|
||||
alt={badge.name}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
<div className="badges-box__item-content">
|
||||
<h3 className="badges-box__item-title">{badge.title}</h3>
|
||||
<p className="badges-box__item-description">
|
||||
{badge.description}
|
||||
</p>
|
||||
</div>
|
||||
return (
|
||||
<div key={badge.name} className="badges-box__item">
|
||||
<div className="badges-box__item-icon">
|
||||
<img
|
||||
src={badge.badge.url}
|
||||
alt={badge.name}
|
||||
width={32}
|
||||
height={32}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hasMoreBadges && (
|
||||
<div className="badges-box__view-all-container">
|
||||
<button
|
||||
type="button"
|
||||
className="badges-box__view-all"
|
||||
onClick={() => setShowAllBadgesModal(true)}
|
||||
>
|
||||
{t("view_all")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div className="badges-box__item-content">
|
||||
<h3 className="badges-box__item-title">{badge.title}</h3>
|
||||
<p className="badges-box__item-description">
|
||||
{badge.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{hasMoreBadges && (
|
||||
<div className="badges-box__view-all-container">
|
||||
<button
|
||||
type="button"
|
||||
className="badges-box__view-all"
|
||||
onClick={() => setShowAllBadgesModal(true)}
|
||||
>
|
||||
{t("view_all")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AllBadgesModal
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
.friends-box {
|
||||
&__section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 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);
|
||||
position: relative;
|
||||
}
|
||||
@@ -35,7 +25,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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { useFormat, useUserDetails } from "@renderer/hooks";
|
||||
import { useUserDetails } from "@renderer/hooks";
|
||||
import { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { PlusIcon } from "@primer/octicons-react";
|
||||
@@ -10,10 +10,9 @@ import { AddFriendModal } from "./add-friend-modal";
|
||||
import "./friends-box.scss";
|
||||
|
||||
export function FriendsBox() {
|
||||
const { userProfile, userStats } = useContext(userProfileContext);
|
||||
const { userProfile } = useContext(userProfileContext);
|
||||
const { userDetails } = useUserDetails();
|
||||
const { t } = useTranslation("user_profile");
|
||||
const { numberFormatter } = useFormat();
|
||||
const [showAllFriendsModal, setShowAllFriendsModal] = useState(false);
|
||||
const [showAddFriendModal, setShowAddFriendModal] = useState(false);
|
||||
|
||||
@@ -38,73 +37,50 @@ export function FriendsBox() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="friends-box__section-header">
|
||||
<div className="profile-content__section-title-group">
|
||||
<h2>{t("friends")}</h2>
|
||||
{userStats && (
|
||||
<span className="profile-content__section-badge">
|
||||
{numberFormatter.format(userStats.friendsCount)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isMe && (
|
||||
<button
|
||||
type="button"
|
||||
className="friends-box__add-friend-button"
|
||||
onClick={() => setShowAddFriendModal(true)}
|
||||
<div className="friends-box__box">
|
||||
<ul className="friends-box__list">
|
||||
{userProfile?.friends.map((friend) => (
|
||||
<li
|
||||
key={friend.id}
|
||||
title={
|
||||
friend.currentGame
|
||||
? t("playing", { game: friend.currentGame.title })
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
{t("add_friends")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="friends-box__box">
|
||||
<ul className="friends-box__list">
|
||||
{userProfile?.friends.map((friend) => (
|
||||
<li
|
||||
key={friend.id}
|
||||
title={
|
||||
friend.currentGame
|
||||
? t("playing", { game: friend.currentGame.title })
|
||||
: undefined
|
||||
}
|
||||
<Link
|
||||
to={`/profile/${friend.id}`}
|
||||
className="friends-box__list-item"
|
||||
>
|
||||
<Link
|
||||
to={`/profile/${friend.id}`}
|
||||
className="friends-box__list-item"
|
||||
>
|
||||
<Avatar
|
||||
size={32}
|
||||
src={friend.profileImageUrl}
|
||||
alt={friend.displayName}
|
||||
/>
|
||||
<Avatar
|
||||
size={32}
|
||||
src={friend.profileImageUrl}
|
||||
alt={friend.displayName}
|
||||
/>
|
||||
|
||||
<div className="friends-box__friend-details">
|
||||
<span className="friends-box__friend-name">
|
||||
{friend.displayName}
|
||||
</span>
|
||||
{friend.currentGame && (
|
||||
<div className="friends-box__game-info">
|
||||
{getGameImage(friend.currentGame)}
|
||||
<small>{friend.currentGame.title}</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="friends-box__view-all-container">
|
||||
<button
|
||||
type="button"
|
||||
className="friends-box__view-all"
|
||||
onClick={() => setShowAllFriendsModal(true)}
|
||||
>
|
||||
{t("view_all")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="friends-box__friend-details">
|
||||
<span className="friends-box__friend-name">
|
||||
{friend.displayName}
|
||||
</span>
|
||||
{friend.currentGame && (
|
||||
<div className="friends-box__game-info">
|
||||
{getGameImage(friend.currentGame)}
|
||||
<small>{friend.currentGame.title}</small>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="friends-box__view-all-container">
|
||||
<button
|
||||
type="button"
|
||||
className="friends-box__view-all"
|
||||
onClick={() => setShowAllFriendsModal(true)}
|
||||
>
|
||||
{t("view_all")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="friends-box__add-friend-button"
|
||||
onClick={() => setShowAddFriendModal(true)}
|
||||
>
|
||||
<PlusIcon size={16} />
|
||||
{t("add_friends")}
|
||||
</button>
|
||||
<AddFriendModal
|
||||
visible={showAddFriendModal}
|
||||
onClose={() => setShowAddFriendModal(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 && (
|
||||
<div className="profile-content__right-content">
|
||||
<UserStatsBox />
|
||||
<BadgesBox />
|
||||
<UserKarmaBox />
|
||||
<RecentGamesBox />
|
||||
<FriendsBox />
|
||||
{userStats && (
|
||||
<ProfileSection title={t("stats")} defaultOpen={true}>
|
||||
<UserStatsBox />
|
||||
</ProfileSection>
|
||||
)}
|
||||
{userProfile?.badges.length > 0 && (
|
||||
<ProfileSection
|
||||
title={t("badges")}
|
||||
count={userProfile.badges.length}
|
||||
defaultOpen={true}
|
||||
>
|
||||
<BadgesBox />
|
||||
</ProfileSection>
|
||||
)}
|
||||
{userProfile?.karma !== undefined &&
|
||||
userProfile?.karma !== null && (
|
||||
<ProfileSection title={t("karma")} defaultOpen={true}>
|
||||
<UserKarmaBox />
|
||||
</ProfileSection>
|
||||
)}
|
||||
{userProfile?.recentGames.length > 0 && (
|
||||
<ProfileSection title={t("activity")} defaultOpen={true}>
|
||||
<RecentGamesBox />
|
||||
</ProfileSection>
|
||||
)}
|
||||
{userProfile?.friends.length > 0 && (
|
||||
<ProfileSection
|
||||
title={t("friends")}
|
||||
count={userStats?.friendsCount || userProfile.friends.length}
|
||||
action={<FriendsBoxAddButton />}
|
||||
defaultOpen={true}
|
||||
>
|
||||
<FriendsBox />
|
||||
</ProfileSection>
|
||||
)}
|
||||
<ReportProfile />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -42,38 +42,32 @@ export function RecentGamesBox() {
|
||||
if (!userProfile?.recentGames.length) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="recent-games__section-header">
|
||||
<h2>{t("activity")}</h2>
|
||||
</div>
|
||||
<div className="recent-games__box">
|
||||
<ul className="recent-games__list">
|
||||
{userProfile?.recentGames.map((game) => (
|
||||
<li key={`${game.shop}-${game.objectId}`}>
|
||||
<Link
|
||||
to={buildUserGameDetailsPath(game)}
|
||||
className="recent-games__list-item"
|
||||
>
|
||||
<img
|
||||
src={game.iconUrl!}
|
||||
alt={game.title}
|
||||
className="recent-games__game-image"
|
||||
/>
|
||||
|
||||
<div className="recent-games__box">
|
||||
<ul className="recent-games__list">
|
||||
{userProfile?.recentGames.map((game) => (
|
||||
<li key={`${game.shop}-${game.objectId}`}>
|
||||
<Link
|
||||
to={buildUserGameDetailsPath(game)}
|
||||
className="recent-games__list-item"
|
||||
>
|
||||
<img
|
||||
src={game.iconUrl!}
|
||||
alt={game.title}
|
||||
className="recent-games__game-image"
|
||||
/>
|
||||
<div className="recent-games__game-details">
|
||||
<span className="recent-games__game-title">{game.title}</span>
|
||||
|
||||
<div className="recent-games__game-details">
|
||||
<span className="recent-games__game-title">{game.title}</span>
|
||||
|
||||
<div className="recent-games__game-description">
|
||||
<ClockIcon />
|
||||
<small>{formatPlayTime(game)}</small>
|
||||
</div>
|
||||
<div className="recent-games__game-description">
|
||||
<ClockIcon />
|
||||
<small>{formatPlayTime(game)}</small>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,24 +18,18 @@ export function UserKarmaBox() {
|
||||
if (karma === undefined || karma === null) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="user-karma__section-header">
|
||||
<h2>{t("karma")}</h2>
|
||||
</div>
|
||||
|
||||
<div className="user-karma__box">
|
||||
<div className="user-karma__content">
|
||||
<div className="user-karma__stats-row">
|
||||
<p className="user-karma__description">
|
||||
<Award size={20} /> {numberFormatter.format(karma)}{" "}
|
||||
{t("karma_count")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="user-karma__info">
|
||||
<small className="user-karma__info-text">
|
||||
{t("karma_description")}
|
||||
</small>
|
||||
</div>
|
||||
<div className="user-karma__box">
|
||||
<div className="user-karma__content">
|
||||
<div className="user-karma__stats-row">
|
||||
<p className="user-karma__description">
|
||||
<Award size={20} /> {numberFormatter.format(karma)}{" "}
|
||||
{t("karma_count")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="user-karma__info">
|
||||
<small className="user-karma__info-text">
|
||||
{t("karma_description")}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -34,87 +34,81 @@ export function UserStatsBox() {
|
||||
if (!userStats) return null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="user-stats__section-header">
|
||||
<h2>{t("stats")}</h2>
|
||||
</div>
|
||||
|
||||
<div className="user-stats__box">
|
||||
<ul className="user-stats__list">
|
||||
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
||||
<li className="user-stats__list-item">
|
||||
<h3 className="user-stats__list-title">
|
||||
{t("achievements_unlocked")}
|
||||
</h3>
|
||||
{userStats.unlockedAchievementSum !== undefined ? (
|
||||
<div className="user-stats__stats-row">
|
||||
<p className="user-stats__list-description">
|
||||
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
|
||||
{t("achievements")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
className="user-stats__link"
|
||||
>
|
||||
<small style={{ color: "var(--color-warning)" }}>
|
||||
{t("show_achievements_on_profile")}
|
||||
</small>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
|
||||
{(isMe || userStats.achievementsPointsEarnedSum !== undefined) && (
|
||||
<li className="user-stats__list-item">
|
||||
<h3 className="user-stats__list-title">{t("earned_points")}</h3>
|
||||
{userStats.achievementsPointsEarnedSum !== undefined ? (
|
||||
<div className="user-stats__stats-row">
|
||||
<p className="user-stats__list-description">
|
||||
<HydraIcon width={20} height={20} />
|
||||
{numberFormatter.format(
|
||||
userStats.achievementsPointsEarnedSum.value
|
||||
)}
|
||||
</p>
|
||||
<p title={t("ranking_updated_weekly")}>
|
||||
{t("top_percentile", {
|
||||
percentile:
|
||||
userStats.achievementsPointsEarnedSum.topPercentile,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showHydraCloudModal("achievements-points")}
|
||||
className="user-stats__link"
|
||||
>
|
||||
<small className="user-stats__link--warning">
|
||||
{t("show_points_on_profile")}
|
||||
</small>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
|
||||
<div className="user-stats__box">
|
||||
<ul className="user-stats__list">
|
||||
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
||||
<li className="user-stats__list-item">
|
||||
<h3 className="user-stats__list-title">{t("total_play_time")}</h3>
|
||||
<div className="user-stats__stats-row">
|
||||
<p className="user-stats__list-description">
|
||||
<ClockIcon />
|
||||
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
|
||||
</p>
|
||||
<p title={t("ranking_updated_weekly")}>
|
||||
{t("top_percentile", {
|
||||
percentile: userStats.totalPlayTimeInSeconds.topPercentile,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<h3 className="user-stats__list-title">
|
||||
{t("achievements_unlocked")}
|
||||
</h3>
|
||||
{userStats.unlockedAchievementSum !== undefined ? (
|
||||
<div className="user-stats__stats-row">
|
||||
<p className="user-stats__list-description">
|
||||
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
|
||||
{t("achievements")}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showHydraCloudModal("achievements")}
|
||||
className="user-stats__link"
|
||||
>
|
||||
<small style={{ color: "var(--color-warning)" }}>
|
||||
{t("show_achievements_on_profile")}
|
||||
</small>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(isMe || userStats.achievementsPointsEarnedSum !== undefined) && (
|
||||
<li className="user-stats__list-item">
|
||||
<h3 className="user-stats__list-title">{t("earned_points")}</h3>
|
||||
{userStats.achievementsPointsEarnedSum !== undefined ? (
|
||||
<div className="user-stats__stats-row">
|
||||
<p className="user-stats__list-description">
|
||||
<HydraIcon width={20} height={20} />
|
||||
{numberFormatter.format(
|
||||
userStats.achievementsPointsEarnedSum.value
|
||||
)}
|
||||
</p>
|
||||
<p title={t("ranking_updated_weekly")}>
|
||||
{t("top_percentile", {
|
||||
percentile:
|
||||
userStats.achievementsPointsEarnedSum.topPercentile,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => showHydraCloudModal("achievements-points")}
|
||||
className="user-stats__link"
|
||||
>
|
||||
<small className="user-stats__link--warning">
|
||||
{t("show_points_on_profile")}
|
||||
</small>
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
)}
|
||||
|
||||
<li className="user-stats__list-item">
|
||||
<h3 className="user-stats__list-title">{t("total_play_time")}</h3>
|
||||
<div className="user-stats__stats-row">
|
||||
<p className="user-stats__list-description">
|
||||
<ClockIcon />
|
||||
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
|
||||
</p>
|
||||
<p title={t("ranking_updated_weekly")}>
|
||||
{t("top_percentile", {
|
||||
percentile: userStats.totalPlayTimeInSeconds.topPercentile,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
</h2>
|
||||
|
||||
<button
|
||||
<motion.button
|
||||
type="button"
|
||||
className="profile-hero__copy-button"
|
||||
onClick={copyFriendCode}
|
||||
title={t("copy_friend_code")}
|
||||
onMouseEnter={() => setIsCopyButtonHovered(true)}
|
||||
onMouseLeave={() => setIsCopyButtonHovered(false)}
|
||||
initial={{ width: 28 }}
|
||||
animate={{
|
||||
width: isCopyButtonHovered ? 94 : 28,
|
||||
}}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
>
|
||||
<CopyIcon size={16} />
|
||||
</button>
|
||||
<motion.span
|
||||
className="profile-hero__friend-code"
|
||||
initial={{ opacity: 0, marginLeft: 0 }}
|
||||
animate={{
|
||||
opacity: isCopyButtonHovered ? 1 : 0,
|
||||
marginLeft: isCopyButtonHovered ? 8 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
>
|
||||
{userProfile?.id}
|
||||
</motion.span>
|
||||
</motion.button>
|
||||
</div>
|
||||
) : (
|
||||
<Skeleton width={150} height={28} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<HTMLDivElement>(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 (
|
||||
<div className="profile-section">
|
||||
<div className="profile-section__header">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="profile-section__button"
|
||||
>
|
||||
<ChevronDownIcon
|
||||
className={`profile-section__chevron ${
|
||||
isOpen ? "profile-section__chevron--open" : ""
|
||||
}`}
|
||||
/>
|
||||
<span>{title}</span>
|
||||
{count !== undefined && (
|
||||
<span className="profile-section__count">{count}</span>
|
||||
)}
|
||||
</button>
|
||||
{action && <div className="profile-section__action">{action}</div>}
|
||||
</div>
|
||||
|
||||
<div
|
||||
ref={content}
|
||||
className="profile-section__content"
|
||||
style={{
|
||||
maxHeight: `${height}px`,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./user-friend-modal";
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
<small>
|
||||
{type == "SENT" ? t("request_sent") : t("request_received")}
|
||||
</small>
|
||||
);
|
||||
};
|
||||
|
||||
const getRequestActions = () => {
|
||||
if (type === null) return null;
|
||||
|
||||
if (type === "SENT") {
|
||||
return (
|
||||
<button
|
||||
className="user-friend-item__cancel-button"
|
||||
onClick={() => props.onClickCancelRequest(userId)}
|
||||
title={t("cancel_request")}
|
||||
>
|
||||
<XCircleIcon size={28} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "RECEIVED") {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="user-friend-item__accept-button"
|
||||
onClick={() => props.onClickAcceptRequest(userId)}
|
||||
title={t("accept_request")}
|
||||
>
|
||||
<CheckCircleIcon size={28} />
|
||||
</button>
|
||||
<button
|
||||
className="user-friend-item__cancel-button"
|
||||
onClick={() => props.onClickRefuseRequest(userId)}
|
||||
title={t("ignore_request")}
|
||||
>
|
||||
<XCircleIcon size={28} />
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "ACCEPTED") {
|
||||
return (
|
||||
<button
|
||||
className="user-friend-item__cancel-button"
|
||||
onClick={() => props.onClickUndoFriendship(userId)}
|
||||
title={t("undo_friendship")}
|
||||
>
|
||||
<XCircleIcon size={28} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === "BLOCKED") {
|
||||
return (
|
||||
<button
|
||||
className="user-friend-item__cancel-button"
|
||||
onClick={() => props.onClickUnblock(userId)}
|
||||
title={t("unblock")}
|
||||
>
|
||||
<XCircleIcon size={28} />
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
if (type === "BLOCKED") {
|
||||
return (
|
||||
<div className="user-friend-item__container">
|
||||
<div className="user-friend-item__button">
|
||||
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
||||
<div className="user-friend-item__button__content">
|
||||
<p className="user-friend-item__display-name">{displayName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="user-friend-item__button__actions">
|
||||
{getRequestActions()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="user-friend-item__container">
|
||||
<button
|
||||
type="button"
|
||||
className="user-friend-item__button"
|
||||
onClick={() => props.onClickItem(userId)}
|
||||
>
|
||||
<Avatar size={35} src={profileImageUrl} alt={displayName} />
|
||||
<div className="user-friend-item__button__content">
|
||||
<p className="user-friend-item__display-name">{displayName}</p>
|
||||
|
||||
{getRequestDescription()}
|
||||
</div>
|
||||
</button>
|
||||
<div className="user-friend-item__button__actions">
|
||||
{getRequestActions()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<HTMLInputElement>) => {
|
||||
const friendCode = e.target.value.trim().slice(0, 8);
|
||||
setFriendCode(friendCode);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="user-friend-modal-add-friend__actions">
|
||||
<TextField
|
||||
label={t("friend_code")}
|
||||
value={friendCode}
|
||||
containerProps={{ style: { width: "100%" } }}
|
||||
onChange={handleChangeFriendCode}
|
||||
/>
|
||||
<Button
|
||||
disabled={isAddingFriend}
|
||||
className="user-friend-modal-add-friend__button"
|
||||
type="button"
|
||||
onClick={() => validateFriendCode(handleClickAddFriend)}
|
||||
>
|
||||
{isAddingFriend ? t("sending") : t("add")}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => validateFriendCode(handleClickSeeProfile)}
|
||||
disabled={isAddingFriend}
|
||||
className="user-friend-modal-add-friend__button"
|
||||
type="button"
|
||||
>
|
||||
{t("see_profile")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="user-friend-modal-add-friend__pending-container">
|
||||
<h3>{t("pending")}</h3>
|
||||
{friendRequests.length === 0 && <p>{t("no_pending_invites")}</p>}
|
||||
{friendRequests.map((request) => {
|
||||
return (
|
||||
<UserFriendItem
|
||||
key={request.id}
|
||||
displayName={request.displayName}
|
||||
type={request.type}
|
||||
profileImageUrl={request.profileImageUrl}
|
||||
userId={request.id}
|
||||
onClickAcceptRequest={handleAcceptFriendRequest}
|
||||
onClickCancelRequest={handleCancelFriendRequest}
|
||||
onClickRefuseRequest={handleRefuseFriendRequest}
|
||||
onClickItem={handleClickRequest}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<UserFriend[]>([]);
|
||||
const listContainer = useRef<HTMLDivElement>(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 (
|
||||
<SkeletonTheme baseColor="#1c1c1c" highlightColor="#444">
|
||||
<div ref={listContainer} className="user-friend-modal-list">
|
||||
{!isLoading && friends.length === 0 && <p>{t("no_friends_added")}</p>}
|
||||
{friends.map((friend) => (
|
||||
<UserFriendItem
|
||||
userId={friend.id}
|
||||
displayName={friend.displayName}
|
||||
profileImageUrl={friend.profileImageUrl}
|
||||
onClickItem={handleClickFriend}
|
||||
type={null}
|
||||
key={"modal" + friend.id}
|
||||
/>
|
||||
))}
|
||||
{isLoading && <Skeleton className="user-friend-modal-list__skeleton" />}
|
||||
</div>
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 <UserFriendModalList userId={userId} closeModal={onClose} />;
|
||||
}
|
||||
|
||||
if (currentTab == UserFriendModalTab.AddFriend) {
|
||||
return <UserFriendModalAddFriend closeModal={onClose} />;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
navigator.clipboard.writeText(userDetails!.id);
|
||||
showSuccessToast(t("friend_code_copied"));
|
||||
}, [userDetails, showSuccessToast, t]);
|
||||
|
||||
return (
|
||||
<Modal visible={visible} title={t("friends")} onClose={onClose}>
|
||||
<div className="user-friend-modal__container">
|
||||
{isMe && (
|
||||
<>
|
||||
<div className="user-friend-modal__header">
|
||||
<p>{t("your_friend_code")}</p>
|
||||
<button
|
||||
className="user-friend-modal__friend-code-button"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
<h3>{userDetails.id}</h3>
|
||||
<CopyIcon />
|
||||
</button>
|
||||
</div>
|
||||
<section className="user-friend-modal__tabs">
|
||||
{tabs.map((tab, index) => (
|
||||
<Button
|
||||
key={tab}
|
||||
theme={index === currentTab ? "primary" : "outline"}
|
||||
onClick={() => setCurrentTab(index)}
|
||||
>
|
||||
{tab}
|
||||
</Button>
|
||||
))}
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
{renderTab()}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user