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 "./get-me";
|
||||||
import "./process-profile-image";
|
import "./process-profile-image";
|
||||||
import "./sync-friend-requests";
|
|
||||||
import "./update-profile";
|
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),
|
ipcRenderer.invoke("updateProfile", updateProfile),
|
||||||
processProfileImage: (imagePath: string) =>
|
processProfileImage: (imagePath: string) =>
|
||||||
ipcRenderer.invoke("processProfileImage", imagePath),
|
ipcRenderer.invoke("processProfileImage", imagePath),
|
||||||
syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"),
|
|
||||||
onSyncFriendRequests: (cb: (friendRequests: FriendRequestSync) => void) => {
|
onSyncFriendRequests: (cb: (friendRequests: FriendRequestSync) => void) => {
|
||||||
const listener = (
|
const listener = (
|
||||||
_event: Electron.IpcRendererEvent,
|
_event: Electron.IpcRendererEvent,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
clearExtraction,
|
clearExtraction,
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
|
|
||||||
import { useSubscription } from "./hooks/use-subscription";
|
import { useSubscription } from "./hooks/use-subscription";
|
||||||
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
|
||||||
import { ArchiveDeletionModal } from "./pages/downloads/archive-deletion-error-modal";
|
import { ArchiveDeletionModal } from "./pages/downloads/archive-deletion-error-modal";
|
||||||
@@ -56,10 +55,6 @@ export function App() {
|
|||||||
const {
|
const {
|
||||||
userDetails,
|
userDetails,
|
||||||
hasActiveSubscription,
|
hasActiveSubscription,
|
||||||
isFriendsModalVisible,
|
|
||||||
friendRequetsModalTab,
|
|
||||||
friendModalUserId,
|
|
||||||
hideFriendsModal,
|
|
||||||
fetchUserDetails,
|
fetchUserDetails,
|
||||||
updateUserDetails,
|
updateUserDetails,
|
||||||
clearUserDetails,
|
clearUserDetails,
|
||||||
@@ -135,7 +130,6 @@ export function App() {
|
|||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
updateUserDetails(response);
|
updateUserDetails(response);
|
||||||
window.electron.syncFriendRequests();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
@@ -152,7 +146,6 @@ export function App() {
|
|||||||
fetchUserDetails().then((response) => {
|
fetchUserDetails().then((response) => {
|
||||||
if (response) {
|
if (response) {
|
||||||
updateUserDetails(response);
|
updateUserDetails(response);
|
||||||
window.electron.syncFriendRequests();
|
|
||||||
showSuccessToast(t("successfully_signed_in"));
|
showSuccessToast(t("successfully_signed_in"));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -305,15 +298,6 @@ export function App() {
|
|||||||
onClose={() => setShowArchiveDeletionModal(false)}
|
onClose={() => setShowArchiveDeletionModal(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{userDetails && (
|
|
||||||
<UserFriendModal
|
|
||||||
visible={isFriendsModalVisible}
|
|
||||||
initialTab={friendRequetsModalTab}
|
|
||||||
onClose={hideFriendsModal}
|
|
||||||
userId={friendModalUserId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
|
||||||
|
|||||||
@@ -46,8 +46,7 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__notification-button,
|
&__notification-button {
|
||||||
&__friends-button {
|
|
||||||
color: globals.$muted-color;
|
color: globals.$muted-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -63,8 +62,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__notification-button-badge,
|
&__notification-button-badge {
|
||||||
&__friends-button-badge {
|
|
||||||
background-color: globals.$success-color;
|
background-color: globals.$success-color;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useNavigate } from "react-router-dom";
|
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 { useAppSelector, useUserDetails } from "@renderer/hooks";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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 SteamLogo from "@renderer/assets/steam-logo.svg?react";
|
||||||
import { Avatar } from "../avatar/avatar";
|
import { Avatar } from "../avatar/avatar";
|
||||||
import { AuthPage } from "@shared";
|
import { AuthPage } from "@shared";
|
||||||
@@ -16,8 +15,7 @@ export function SidebarProfile() {
|
|||||||
|
|
||||||
const { t } = useTranslation("sidebar");
|
const { t } = useTranslation("sidebar");
|
||||||
|
|
||||||
const { userDetails, friendRequestCount, showFriendsModal } =
|
const { userDetails } = useUserDetails();
|
||||||
useUserDetails();
|
|
||||||
|
|
||||||
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
const { gameRunning } = useAppSelector((state) => state.gameRunning);
|
||||||
|
|
||||||
@@ -114,29 +112,6 @@ export function SidebarProfile() {
|
|||||||
);
|
);
|
||||||
}, [t, notificationCount, navigate]);
|
}, [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 = () => {
|
const gameRunningDetails = () => {
|
||||||
if (!userDetails || !gameRunning) return null;
|
if (!userDetails || !gameRunning) return null;
|
||||||
|
|
||||||
@@ -185,7 +160,6 @@ export function SidebarProfile() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{notificationsButton}
|
{notificationsButton}
|
||||||
{friendsButton}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
1
src/renderer/src/declaration.d.ts
vendored
1
src/renderer/src/declaration.d.ts
vendored
@@ -389,7 +389,6 @@ declare global {
|
|||||||
processProfileImage: (
|
processProfileImage: (
|
||||||
path: string
|
path: string
|
||||||
) => Promise<{ imagePath: string; mimeType: string }>;
|
) => Promise<{ imagePath: string; mimeType: string }>;
|
||||||
syncFriendRequests: () => Promise<void>;
|
|
||||||
onSyncFriendRequests: (
|
onSyncFriendRequests: (
|
||||||
cb: (friendRequests: FriendRequestSync) => void
|
cb: (friendRequests: FriendRequestSync) => void
|
||||||
) => () => Electron.IpcRenderer;
|
) => () => Electron.IpcRenderer;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
import { PayloadAction, createSlice } from "@reduxjs/toolkit";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
|
||||||
import type { FriendRequest, UserDetails } from "@types";
|
import type { FriendRequest, UserDetails } from "@types";
|
||||||
|
|
||||||
export interface UserDetailsState {
|
export interface UserDetailsState {
|
||||||
@@ -7,9 +6,6 @@ export interface UserDetailsState {
|
|||||||
profileBackground: null | string;
|
profileBackground: null | string;
|
||||||
friendRequests: FriendRequest[];
|
friendRequests: FriendRequest[];
|
||||||
friendRequestCount: number;
|
friendRequestCount: number;
|
||||||
isFriendsModalVisible: boolean;
|
|
||||||
friendRequetsModalTab: UserFriendModalTab | null;
|
|
||||||
friendModalUserId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: UserDetailsState = {
|
const initialState: UserDetailsState = {
|
||||||
@@ -17,9 +13,6 @@ const initialState: UserDetailsState = {
|
|||||||
profileBackground: null,
|
profileBackground: null,
|
||||||
friendRequests: [],
|
friendRequests: [],
|
||||||
friendRequestCount: 0,
|
friendRequestCount: 0,
|
||||||
isFriendsModalVisible: false,
|
|
||||||
friendRequetsModalTab: null,
|
|
||||||
friendModalUserId: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const userDetailsSlice = createSlice({
|
export const userDetailsSlice = createSlice({
|
||||||
@@ -38,18 +31,6 @@ export const userDetailsSlice = createSlice({
|
|||||||
setFriendRequestCount: (state, action: PayloadAction<number>) => {
|
setFriendRequestCount: (state, action: PayloadAction<number>) => {
|
||||||
state.friendRequestCount = action.payload;
|
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,
|
setProfileBackground,
|
||||||
setFriendRequests,
|
setFriendRequests,
|
||||||
setFriendRequestCount,
|
setFriendRequestCount,
|
||||||
setFriendsModalVisible,
|
|
||||||
setFriendsModalHidden,
|
|
||||||
} = userDetailsSlice.actions;
|
} = userDetailsSlice.actions;
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import {
|
|||||||
setProfileBackground,
|
setProfileBackground,
|
||||||
setUserDetails,
|
setUserDetails,
|
||||||
setFriendRequests,
|
setFriendRequests,
|
||||||
setFriendsModalVisible,
|
|
||||||
setFriendsModalHidden,
|
|
||||||
} from "@renderer/features";
|
} from "@renderer/features";
|
||||||
import type {
|
import type {
|
||||||
FriendRequestAction,
|
FriendRequestAction,
|
||||||
@@ -13,20 +11,12 @@ import type {
|
|||||||
UserDetails,
|
UserDetails,
|
||||||
FriendRequest,
|
FriendRequest,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
|
|
||||||
|
|
||||||
export function useUserDetails() {
|
export function useUserDetails() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const {
|
const { userDetails, profileBackground, friendRequests, friendRequestCount } =
|
||||||
userDetails,
|
useAppSelector((state) => state.userDetails);
|
||||||
profileBackground,
|
|
||||||
friendRequests,
|
|
||||||
friendRequestCount,
|
|
||||||
isFriendsModalVisible,
|
|
||||||
friendModalUserId,
|
|
||||||
friendRequetsModalTab,
|
|
||||||
} = useAppSelector((state) => state.userDetails);
|
|
||||||
|
|
||||||
const clearUserDetails = useCallback(async () => {
|
const clearUserDetails = useCallback(async () => {
|
||||||
dispatch(setUserDetails(null));
|
dispatch(setUserDetails(null));
|
||||||
@@ -85,24 +75,11 @@ export function useUserDetails() {
|
|||||||
return window.electron.hydraApi
|
return window.electron.hydraApi
|
||||||
.get<FriendRequest[]>("/profile/friend-requests")
|
.get<FriendRequest[]>("/profile/friend-requests")
|
||||||
.then((friendRequests) => {
|
.then((friendRequests) => {
|
||||||
window.electron.syncFriendRequests();
|
|
||||||
dispatch(setFriendRequests(friendRequests));
|
dispatch(setFriendRequests(friendRequests));
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const showFriendsModal = useCallback(
|
|
||||||
(initialTab: UserFriendModalTab, userId: string) => {
|
|
||||||
dispatch(setFriendsModalVisible({ initialTab, userId }));
|
|
||||||
fetchFriendRequests();
|
|
||||||
},
|
|
||||||
[dispatch, fetchFriendRequests]
|
|
||||||
);
|
|
||||||
|
|
||||||
const hideFriendsModal = useCallback(() => {
|
|
||||||
dispatch(setFriendsModalHidden());
|
|
||||||
}, [dispatch]);
|
|
||||||
|
|
||||||
const sendFriendRequest = useCallback(
|
const sendFriendRequest = useCallback(
|
||||||
async (userId: string) => {
|
async (userId: string) => {
|
||||||
return window.electron.hydraApi
|
return window.electron.hydraApi
|
||||||
@@ -152,12 +129,7 @@ export function useUserDetails() {
|
|||||||
profileBackground,
|
profileBackground,
|
||||||
friendRequests,
|
friendRequests,
|
||||||
friendRequestCount,
|
friendRequestCount,
|
||||||
friendRequetsModalTab,
|
|
||||||
isFriendsModalVisible,
|
|
||||||
friendModalUserId,
|
|
||||||
hasActiveSubscription,
|
hasActiveSubscription,
|
||||||
showFriendsModal,
|
|
||||||
hideFriendsModal,
|
|
||||||
fetchUserDetails,
|
fetchUserDetails,
|
||||||
signOut,
|
signOut,
|
||||||
clearUserDetails,
|
clearUserDetails,
|
||||||
|
|||||||
@@ -343,13 +343,17 @@ export default function Notifications() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
const renderContent = () => {
|
||||||
<>
|
if (isLoading && mergedNotifications.length === 0) {
|
||||||
{isLoading && mergedNotifications.length === 0 ? (
|
return (
|
||||||
<div className="notifications__loading">
|
<div className="notifications__loading">
|
||||||
<span>{t("loading")}</span>
|
<span>{t("loading")}</span>
|
||||||
</div>
|
</div>
|
||||||
) : mergedNotifications.length === 0 ? (
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mergedNotifications.length === 0) {
|
||||||
|
return (
|
||||||
<div className="notifications__empty">
|
<div className="notifications__empty">
|
||||||
<div className="notifications__icon-container">
|
<div className="notifications__icon-container">
|
||||||
<BellIcon size={24} />
|
<BellIcon size={24} />
|
||||||
@@ -357,36 +361,40 @@ export default function Notifications() {
|
|||||||
<h2>{t("empty_title")}</h2>
|
<h2>{t("empty_title")}</h2>
|
||||||
<p>{t("empty_description")}</p>
|
<p>{t("empty_description")}</p>
|
||||||
</div>
|
</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">
|
return (
|
||||||
<AnimatePresence mode="popLayout">
|
<div className="notifications">
|
||||||
{displayedNotifications.map(renderNotification)}
|
<div className="notifications__actions">
|
||||||
</AnimatePresence>
|
<Button theme="outline" onClick={handleMarkAllAsRead}>
|
||||||
</div>
|
{t("mark_all_as_read")}
|
||||||
|
</Button>
|
||||||
{pagination.hasMore && (
|
<Button theme="danger" onClick={handleClearAll}>
|
||||||
<div className="notifications__load-more">
|
{t("clear_all")}
|
||||||
<Button
|
</Button>
|
||||||
theme="outline"
|
|
||||||
onClick={handleLoadMore}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? t("loading") : t("load_more")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</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;
|
overflow-y: auto;
|
||||||
padding-right: globals.$spacing-unit;
|
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 { useToast, useUserDetails } from "@renderer/hooks";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { UserFriendItem } from "@renderer/pages/shared-modals/user-friend-modal/user-friend-item";
|
|
||||||
import "./add-friend-modal.scss";
|
import "./add-friend-modal.scss";
|
||||||
|
|
||||||
interface AddFriendModalProps {
|
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 sentRequests = friendRequests.filter((req) => req.type === "SENT");
|
||||||
const currentRequest =
|
const currentRequest =
|
||||||
friendCode.length === 8
|
friendCode.length === 8
|
||||||
@@ -139,17 +122,31 @@ export function AddFriendModal({ visible, onClose }: AddFriendModalProps) {
|
|||||||
<h3>{t("pending")}</h3>
|
<h3>{t("pending")}</h3>
|
||||||
<div className="add-friend-modal__pending-list">
|
<div className="add-friend-modal__pending-list">
|
||||||
{sentRequests.map((request) => (
|
{sentRequests.map((request) => (
|
||||||
<UserFriendItem
|
<button
|
||||||
key={request.id}
|
key={request.id}
|
||||||
displayName={request.displayName}
|
type="button"
|
||||||
type={request.type}
|
className="add-friend-modal__friend-item"
|
||||||
profileImageUrl={request.profileImageUrl}
|
onClick={() => handleClickRequest(request.id)}
|
||||||
userId={request.id}
|
>
|
||||||
onClickAcceptRequest={handleAcceptFriendRequest}
|
<Avatar
|
||||||
onClickCancelRequest={handleCancelFriendRequest}
|
src={request.profileImageUrl}
|
||||||
onClickRefuseRequest={handleRefuseFriendRequest}
|
alt={request.displayName}
|
||||||
onClickItem={handleClickRequest}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
@use "../../../scss/globals.scss";
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
.badges-box {
|
.badges-box {
|
||||||
&__section-header {
|
&__box {
|
||||||
display: flex;
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__box {
|
&__header {
|
||||||
background-color: globals.$background-color;
|
display: flex;
|
||||||
border-radius: 4px;
|
justify-content: flex-end;
|
||||||
border: solid 1px globals.$border-color;
|
margin-bottom: calc(globals.$spacing-unit * 2);
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__list {
|
&__list {
|
||||||
@@ -24,7 +20,7 @@
|
|||||||
&__item {
|
&__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: calc(globals.$spacing-unit * 2);
|
gap: calc(globals.$spacing-unit * 1.5);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: calc(globals.$spacing-unit * 1.5);
|
padding: calc(globals.$spacing-unit * 1.5);
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
@@ -38,8 +34,8 @@
|
|||||||
|
|
||||||
&__item-icon {
|
&__item-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 48px;
|
width: 34px;
|
||||||
height: 48px;
|
height: 34px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -48,8 +44,8 @@
|
|||||||
background-color: globals.$background-color;
|
background-color: globals.$background-color;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
width: 32px;
|
width: 28px;
|
||||||
height: 32px;
|
height: 28px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__item-title {
|
&__item-title {
|
||||||
font-size: globals.$body-font-size;
|
font-size: 0.8rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: globals.$body-color;
|
color: globals.$body-color;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -76,7 +72,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__view-all-container {
|
&__view-all-container {
|
||||||
background-color: globals.$background-color;
|
|
||||||
padding-top: calc(globals.$spacing-unit * 2);
|
padding-top: calc(globals.$spacing-unit * 2);
|
||||||
margin-top: calc(globals.$spacing-unit * 2);
|
margin-top: calc(globals.$spacing-unit * 2);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import { useFormat } from "@renderer/hooks";
|
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AllBadgesModal } from "./all-badges-modal";
|
import { AllBadgesModal } from "./all-badges-modal";
|
||||||
@@ -10,7 +9,6 @@ const MAX_VISIBLE_BADGES = 4;
|
|||||||
export function BadgesBox() {
|
export function BadgesBox() {
|
||||||
const { userProfile, badges } = useContext(userProfileContext);
|
const { userProfile, badges } = useContext(userProfileContext);
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
const { numberFormatter } = useFormat();
|
|
||||||
const [showAllBadgesModal, setShowAllBadgesModal] = useState(false);
|
const [showAllBadgesModal, setShowAllBadgesModal] = useState(false);
|
||||||
|
|
||||||
if (!userProfile?.badges.length) return null;
|
if (!userProfile?.badges.length) return null;
|
||||||
@@ -20,55 +18,44 @@ export function BadgesBox() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div className="badges-box__box">
|
||||||
<div className="badges-box__section-header">
|
<div className="badges-box__list">
|
||||||
<div className="profile-content__section-title-group">
|
{visibleBadges.map((badgeName) => {
|
||||||
<h2>{t("badges")}</h2>
|
const badge = badges.find((b) => b.name === badgeName);
|
||||||
<span className="profile-content__section-badge">
|
|
||||||
{numberFormatter.format(userProfile.badges.length)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="badges-box__box">
|
if (!badge) return null;
|
||||||
<div className="badges-box__list">
|
|
||||||
{visibleBadges.map((badgeName) => {
|
|
||||||
const badge = badges.find((b) => b.name === badgeName);
|
|
||||||
|
|
||||||
if (!badge) return null;
|
return (
|
||||||
|
<div key={badge.name} className="badges-box__item">
|
||||||
return (
|
<div className="badges-box__item-icon">
|
||||||
<div key={badge.name} className="badges-box__item">
|
<img
|
||||||
<div className="badges-box__item-icon">
|
src={badge.badge.url}
|
||||||
<img
|
alt={badge.name}
|
||||||
src={badge.badge.url}
|
width={32}
|
||||||
alt={badge.name}
|
height={32}
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<div className="badges-box__item-content">
|
||||||
})}
|
<h3 className="badges-box__item-title">{badge.title}</h3>
|
||||||
</div>
|
<p className="badges-box__item-description">
|
||||||
{hasMoreBadges && (
|
{badge.description}
|
||||||
<div className="badges-box__view-all-container">
|
</p>
|
||||||
<button
|
</div>
|
||||||
type="button"
|
</div>
|
||||||
className="badges-box__view-all"
|
);
|
||||||
onClick={() => setShowAllBadgesModal(true)}
|
})}
|
||||||
>
|
|
||||||
{t("view_all")}
|
|
||||||
</button>
|
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<AllBadgesModal
|
<AllBadgesModal
|
||||||
|
|||||||
@@ -1,17 +1,7 @@
|
|||||||
@use "../../../scss/globals.scss";
|
@use "../../../scss/globals.scss";
|
||||||
|
|
||||||
.friends-box {
|
.friends-box {
|
||||||
&__section-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-bottom: calc(globals.$spacing-unit * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
&__box {
|
&__box {
|
||||||
background-color: globals.$background-color;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: solid 1px globals.$border-color;
|
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
padding: calc(globals.$spacing-unit * 2);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -35,7 +25,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&__view-all-container {
|
&__view-all-container {
|
||||||
background-color: globals.$background-color;
|
|
||||||
padding-top: calc(globals.$spacing-unit * 2);
|
padding-top: calc(globals.$spacing-unit * 2);
|
||||||
margin-top: calc(globals.$spacing-unit * 2);
|
margin-top: calc(globals.$spacing-unit * 2);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { userProfileContext } from "@renderer/context";
|
import { userProfileContext } from "@renderer/context";
|
||||||
import { useFormat, useUserDetails } from "@renderer/hooks";
|
import { useUserDetails } from "@renderer/hooks";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { PlusIcon } from "@primer/octicons-react";
|
import { PlusIcon } from "@primer/octicons-react";
|
||||||
@@ -10,10 +10,9 @@ import { AddFriendModal } from "./add-friend-modal";
|
|||||||
import "./friends-box.scss";
|
import "./friends-box.scss";
|
||||||
|
|
||||||
export function FriendsBox() {
|
export function FriendsBox() {
|
||||||
const { userProfile, userStats } = useContext(userProfileContext);
|
const { userProfile } = useContext(userProfileContext);
|
||||||
const { userDetails } = useUserDetails();
|
const { userDetails } = useUserDetails();
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
const { numberFormatter } = useFormat();
|
|
||||||
const [showAllFriendsModal, setShowAllFriendsModal] = useState(false);
|
const [showAllFriendsModal, setShowAllFriendsModal] = useState(false);
|
||||||
const [showAddFriendModal, setShowAddFriendModal] = useState(false);
|
const [showAddFriendModal, setShowAddFriendModal] = useState(false);
|
||||||
|
|
||||||
@@ -38,73 +37,50 @@ export function FriendsBox() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div className="friends-box__box">
|
||||||
<div className="friends-box__section-header">
|
<ul className="friends-box__list">
|
||||||
<div className="profile-content__section-title-group">
|
{userProfile?.friends.map((friend) => (
|
||||||
<h2>{t("friends")}</h2>
|
<li
|
||||||
{userStats && (
|
key={friend.id}
|
||||||
<span className="profile-content__section-badge">
|
title={
|
||||||
{numberFormatter.format(userStats.friendsCount)}
|
friend.currentGame
|
||||||
</span>
|
? t("playing", { game: friend.currentGame.title })
|
||||||
)}
|
: undefined
|
||||||
</div>
|
}
|
||||||
{isMe && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="friends-box__add-friend-button"
|
|
||||||
onClick={() => setShowAddFriendModal(true)}
|
|
||||||
>
|
>
|
||||||
<PlusIcon size={16} />
|
<Link
|
||||||
{t("add_friends")}
|
to={`/profile/${friend.id}`}
|
||||||
</button>
|
className="friends-box__list-item"
|
||||||
)}
|
|
||||||
</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
|
<Avatar
|
||||||
to={`/profile/${friend.id}`}
|
size={32}
|
||||||
className="friends-box__list-item"
|
src={friend.profileImageUrl}
|
||||||
>
|
alt={friend.displayName}
|
||||||
<Avatar
|
/>
|
||||||
size={32}
|
|
||||||
src={friend.profileImageUrl}
|
|
||||||
alt={friend.displayName}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="friends-box__friend-details">
|
<div className="friends-box__friend-details">
|
||||||
<span className="friends-box__friend-name">
|
<span className="friends-box__friend-name">
|
||||||
{friend.displayName}
|
{friend.displayName}
|
||||||
</span>
|
</span>
|
||||||
{friend.currentGame && (
|
{friend.currentGame && (
|
||||||
<div className="friends-box__game-info">
|
<div className="friends-box__game-info">
|
||||||
{getGameImage(friend.currentGame)}
|
{getGameImage(friend.currentGame)}
|
||||||
<small>{friend.currentGame.title}</small>
|
<small>{friend.currentGame.title}</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="friends-box__view-all-container">
|
<div className="friends-box__view-all-container">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="friends-box__view-all"
|
className="friends-box__view-all"
|
||||||
onClick={() => setShowAllFriendsModal(true)}
|
onClick={() => setShowAllFriendsModal(true)}
|
||||||
>
|
>
|
||||||
{t("view_all")}
|
{t("view_all")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</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 { LockedProfile } from "./locked-profile";
|
||||||
import { ReportProfile } from "../report-profile/report-profile";
|
import { ReportProfile } from "../report-profile/report-profile";
|
||||||
import { BadgesBox } from "./badges-box";
|
import { BadgesBox } from "./badges-box";
|
||||||
import { FriendsBox } from "./friends-box";
|
import { FriendsBox, FriendsBoxAddButton } from "./friends-box";
|
||||||
import { RecentGamesBox } from "./recent-games-box";
|
import { RecentGamesBox } from "./recent-games-box";
|
||||||
import { UserStatsBox } from "./user-stats-box";
|
import { UserStatsBox } from "./user-stats-box";
|
||||||
import { UserKarmaBox } from "./user-karma-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 { DeleteReviewModal } from "@renderer/pages/game-details/modals/delete-review-modal";
|
||||||
import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations";
|
import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations";
|
||||||
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
@@ -187,8 +188,6 @@ export function ProfileContent() {
|
|||||||
);
|
);
|
||||||
setReviews(response.reviews);
|
setReviews(response.reviews);
|
||||||
setReviewsTotalCount(response.totalCount);
|
setReviewsTotalCount(response.totalCount);
|
||||||
} catch (error) {
|
|
||||||
// Error handling for fetching reviews
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsLoadingReviews(false);
|
setIsLoadingReviews(false);
|
||||||
}
|
}
|
||||||
@@ -427,11 +426,41 @@ export function ProfileContent() {
|
|||||||
|
|
||||||
{shouldShowRightContent && (
|
{shouldShowRightContent && (
|
||||||
<div className="profile-content__right-content">
|
<div className="profile-content__right-content">
|
||||||
<UserStatsBox />
|
{userStats && (
|
||||||
<BadgesBox />
|
<ProfileSection title={t("stats")} defaultOpen={true}>
|
||||||
<UserKarmaBox />
|
<UserStatsBox />
|
||||||
<RecentGamesBox />
|
</ProfileSection>
|
||||||
<FriendsBox />
|
)}
|
||||||
|
{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 />
|
<ReportProfile />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,19 +2,9 @@
|
|||||||
|
|
||||||
.recent-games {
|
.recent-games {
|
||||||
&__box {
|
&__box {
|
||||||
background-color: globals.$background-color;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: solid 1px globals.$border-color;
|
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
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 {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -42,38 +42,32 @@ export function RecentGamesBox() {
|
|||||||
if (!userProfile?.recentGames.length) return null;
|
if (!userProfile?.recentGames.length) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="recent-games__box">
|
||||||
<div className="recent-games__section-header">
|
<ul className="recent-games__list">
|
||||||
<h2>{t("activity")}</h2>
|
{userProfile?.recentGames.map((game) => (
|
||||||
</div>
|
<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">
|
<div className="recent-games__game-details">
|
||||||
<ul className="recent-games__list">
|
<span className="recent-games__game-title">{game.title}</span>
|
||||||
{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">
|
<div className="recent-games__game-description">
|
||||||
<span className="recent-games__game-title">{game.title}</span>
|
<ClockIcon />
|
||||||
|
<small>{formatPlayTime(game)}</small>
|
||||||
<div className="recent-games__game-description">
|
|
||||||
<ClockIcon />
|
|
||||||
<small>{formatPlayTime(game)}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</div>
|
||||||
</li>
|
</Link>
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,9 @@
|
|||||||
|
|
||||||
.user-karma {
|
.user-karma {
|
||||||
&__box {
|
&__box {
|
||||||
background-color: globals.$background-color;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: solid 1px globals.$border-color;
|
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
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 {
|
&__content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -18,24 +18,18 @@ export function UserKarmaBox() {
|
|||||||
if (karma === undefined || karma === null) return null;
|
if (karma === undefined || karma === null) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="user-karma__box">
|
||||||
<div className="user-karma__section-header">
|
<div className="user-karma__content">
|
||||||
<h2>{t("karma")}</h2>
|
<div className="user-karma__stats-row">
|
||||||
</div>
|
<p className="user-karma__description">
|
||||||
|
<Award size={20} /> {numberFormatter.format(karma)}{" "}
|
||||||
<div className="user-karma__box">
|
{t("karma_count")}
|
||||||
<div className="user-karma__content">
|
</p>
|
||||||
<div className="user-karma__stats-row">
|
</div>
|
||||||
<p className="user-karma__description">
|
<div className="user-karma__info">
|
||||||
<Award size={20} /> {numberFormatter.format(karma)}{" "}
|
<small className="user-karma__info-text">
|
||||||
{t("karma_count")}
|
{t("karma_description")}
|
||||||
</p>
|
</small>
|
||||||
</div>
|
|
||||||
<div className="user-karma__info">
|
|
||||||
<small className="user-karma__info-text">
|
|
||||||
{t("karma_description")}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,19 +2,9 @@
|
|||||||
|
|
||||||
.user-stats {
|
.user-stats {
|
||||||
&__box {
|
&__box {
|
||||||
background-color: globals.$background-color;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: solid 1px globals.$border-color;
|
|
||||||
padding: calc(globals.$spacing-unit * 2);
|
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 {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
@@ -34,87 +34,81 @@ export function UserStatsBox() {
|
|||||||
if (!userStats) return null;
|
if (!userStats) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="user-stats__box">
|
||||||
<div className="user-stats__section-header">
|
<ul className="user-stats__list">
|
||||||
<h2>{t("stats")}</h2>
|
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
||||||
</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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<li className="user-stats__list-item">
|
<li className="user-stats__list-item">
|
||||||
<h3 className="user-stats__list-title">{t("total_play_time")}</h3>
|
<h3 className="user-stats__list-title">
|
||||||
<div className="user-stats__stats-row">
|
{t("achievements_unlocked")}
|
||||||
<p className="user-stats__list-description">
|
</h3>
|
||||||
<ClockIcon />
|
{userStats.unlockedAchievementSum !== undefined ? (
|
||||||
{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}
|
<div className="user-stats__stats-row">
|
||||||
</p>
|
<p className="user-stats__list-description">
|
||||||
<p title={t("ranking_updated_weekly")}>
|
<TrophyIcon /> {userStats.unlockedAchievementSum}{" "}
|
||||||
{t("top_percentile", {
|
{t("achievements")}
|
||||||
percentile: userStats.totalPlayTimeInSeconds.topPercentile,
|
</p>
|
||||||
})}
|
</div>
|
||||||
</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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
&__copy-button {
|
&__copy-button {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
color: globals.$body-color;
|
color: globals.$body-color;
|
||||||
@@ -38,7 +38,11 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: calc(globals.$spacing-unit / 1.5);
|
padding: calc(globals.$spacing-unit / 1.5);
|
||||||
border-radius: 6px;
|
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 {
|
&:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -46,6 +50,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__friend-code {
|
||||||
|
font-size: globals.$small-font-size;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
&__user-information {
|
&__user-information {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: calc(globals.$spacing-unit * 7) calc(globals.$spacing-unit * 3);
|
padding: calc(globals.$spacing-unit * 7) calc(globals.$spacing-unit * 3);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
} from "@renderer/hooks";
|
} from "@renderer/hooks";
|
||||||
import { addSeconds } from "date-fns";
|
import { addSeconds } from "date-fns";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
import type { FriendRequestAction } from "@types";
|
import type { FriendRequestAction } from "@types";
|
||||||
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||||
@@ -34,6 +35,7 @@ type FriendAction =
|
|||||||
export function ProfileHero() {
|
export function ProfileHero() {
|
||||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||||
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||||
|
const [isCopyButtonHovered, setIsCopyButtonHovered] = useState(false);
|
||||||
|
|
||||||
const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } =
|
const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } =
|
||||||
useContext(userProfileContext);
|
useContext(userProfileContext);
|
||||||
@@ -312,14 +314,32 @@ export function ProfileHero() {
|
|||||||
{userProfile?.displayName}
|
{userProfile?.displayName}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<button
|
<motion.button
|
||||||
type="button"
|
type="button"
|
||||||
className="profile-hero__copy-button"
|
className="profile-hero__copy-button"
|
||||||
onClick={copyFriendCode}
|
onClick={copyFriendCode}
|
||||||
title={t("copy_friend_code")}
|
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} />
|
<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>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Skeleton width={150} height={28} />
|
<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