diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index 4f5253f1..5c058252 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -324,7 +324,8 @@ export function Header() { 0 || + (searchValue.trim().length > 0 || + historyItems.length > 0 || suggestions.length > 0 || isLoadingSuggestions) } diff --git a/src/renderer/src/components/search-dropdown/search-dropdown.scss b/src/renderer/src/components/search-dropdown/search-dropdown.scss index 40a55432..6a2cbede 100644 --- a/src/renderer/src/components/search-dropdown/search-dropdown.scss +++ b/src/renderer/src/components/search-dropdown/search-dropdown.scss @@ -5,7 +5,7 @@ background-color: globals.$dark-background-color; border: 1px solid globals.$border-color; border-radius: 8px; - max-height: 300px; + max-height: 350px; overflow-y: auto; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); z-index: 1000; diff --git a/src/renderer/src/components/search-dropdown/search-dropdown.tsx b/src/renderer/src/components/search-dropdown/search-dropdown.tsx index 4142e4a5..cc7ce5b4 100644 --- a/src/renderer/src/components/search-dropdown/search-dropdown.tsx +++ b/src/renderer/src/components/search-dropdown/search-dropdown.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useCallback, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { ClockIcon, SearchIcon, XIcon } from "@primer/octicons-react"; import cn from "classnames"; @@ -92,23 +92,8 @@ export function SearchDropdown({ return () => document.removeEventListener("mousedown", handleClickOutside); }, [visible, onClose, searchContainerRef]); - const handleItemClick = useCallback( - ( - type: "history" | "suggestion", - item: SearchHistoryEntry | SearchSuggestion - ) => { - if (type === "history") { - onSelectHistory((item as SearchHistoryEntry).query); - } else { - onSelectSuggestion(item as SearchSuggestion); - } - }, - [onSelectHistory, onSelectSuggestion] - ); - if (!visible) return null; - const totalItems = historyItems.length + suggestions.length; const hasHistory = historyItems.length > 0; const hasSuggestions = suggestions.length > 0; @@ -158,7 +143,7 @@ export function SearchDropdown({ activeIndex === getItemIndex("history", index), })} onMouseDown={(e) => e.preventDefault()} - onClick={() => handleItemClick("history", item)} + onClick={() => onSelectHistory(item.query)} > @@ -200,7 +185,7 @@ export function SearchDropdown({ activeIndex === getItemIndex("suggestion", index), })} onMouseDown={(e) => e.preventDefault()} - onClick={() => handleItemClick("suggestion", item)} + onClick={() => onSelectSuggestion(item)} > {item.iconUrl ? ( {t("loading")} )} - - {!isLoadingSuggestions && - !hasHistory && - !hasSuggestions && - totalItems === 0 && ( -
{t("no_results")}
- )} ); diff --git a/src/renderer/src/pages/profile/profile-content/add-friend-modal.scss b/src/renderer/src/pages/profile/profile-content/add-friend-modal.scss new file mode 100644 index 00000000..a2db81ef --- /dev/null +++ b/src/renderer/src/pages/profile/profile-content/add-friend-modal.scss @@ -0,0 +1,54 @@ +@use "../../../scss/globals.scss"; + +.add-friend-modal { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 2); + width: 100%; + min-width: 400px; + + &__actions { + display: flex; + flex-direction: row; + align-items: flex-end; + gap: globals.$spacing-unit; + } + + &__button { + align-self: flex-end; + white-space: nowrap; + } + + &__pending-status { + color: globals.$body-color; + font-size: globals.$small-font-size; + text-align: center; + padding: calc(globals.$spacing-unit / 2); + background-color: rgba(255, 255, 255, 0.05); + border-radius: 4px; + margin-top: calc(globals.$spacing-unit * -1); + } + + &__pending-container { + display: flex; + flex-direction: column; + gap: calc(globals.$spacing-unit * 2); + margin-top: calc(globals.$spacing-unit * 2); + + h3 { + margin: 0; + font-size: globals.$body-font-size; + font-weight: 600; + color: globals.$muted-color; + } + } + + &__pending-list { + display: flex; + flex-direction: column; + gap: globals.$spacing-unit; + max-height: 300px; + overflow-y: auto; + padding-right: globals.$spacing-unit; + } +} diff --git a/src/renderer/src/pages/profile/profile-content/add-friend-modal.tsx b/src/renderer/src/pages/profile/profile-content/add-friend-modal.tsx new file mode 100644 index 00000000..04838f6f --- /dev/null +++ b/src/renderer/src/pages/profile/profile-content/add-friend-modal.tsx @@ -0,0 +1,160 @@ +import { 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 { + readonly visible: boolean; + readonly onClose: () => void; +} + +export function AddFriendModal({ visible, onClose }: AddFriendModalProps) { + const { t } = useTranslation("user_profile"); + const navigate = useNavigate(); + + const [friendCode, setFriendCode] = useState(""); + const [isAddingFriend, setIsAddingFriend] = useState(false); + + const { + sendFriendRequest, + updateFriendRequestState, + friendRequests, + fetchFriendRequests, + } = useUserDetails(); + + const { showSuccessToast, showErrorToast } = useToast(); + + useEffect(() => { + if (visible) { + setFriendCode(""); + fetchFriendRequests(); + } + }, [visible, fetchFriendRequests]); + + const handleChangeFriendCode = (e: React.ChangeEvent) => { + const code = e.target.value.trim().slice(0, 8); + setFriendCode(code); + }; + + const validateFriendCode = (callback: () => void) => { + if (friendCode.length === 8) { + return callback(); + } + + showErrorToast(t("friend_code_length_error")); + }; + + const handleClickAddFriend = () => { + setIsAddingFriend(true); + sendFriendRequest(friendCode) + .then(() => { + setFriendCode(""); + showSuccessToast(t("request_sent")); + }) + .catch(() => { + showErrorToast(t("error_adding_friend")); + }) + .finally(() => { + setIsAddingFriend(false); + }); + }; + + const handleClickSeeProfile = () => { + if (friendCode.length === 8) { + onClose(); + navigate(`/profile/${friendCode}`); + } + }; + + const handleClickRequest = (userId: string) => { + onClose(); + navigate(`/profile/${userId}`); + }; + + 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 sentRequests = friendRequests.filter((req) => req.type === "SENT"); + const currentRequest = + friendCode.length === 8 + ? sentRequests.find((req) => req.id === friendCode) + : null; + + return ( + +
+
+ + + +
+ {currentRequest && ( +
{t("pending")}
+ )} + + {sentRequests.length > 0 && ( +
+

{t("pending")}

+
+ {sentRequests.map((request) => ( + + ))} +
+
+ )} +
+
+ ); +} diff --git a/src/renderer/src/pages/profile/profile-content/all-friends-modal.scss b/src/renderer/src/pages/profile/profile-content/all-friends-modal.scss index 06bbd2ee..8ecbaa46 100644 --- a/src/renderer/src/pages/profile/profile-content/all-friends-modal.scss +++ b/src/renderer/src/pages/profile/profile-content/all-friends-modal.scss @@ -98,27 +98,4 @@ justify-content: center; padding-top: globals.$spacing-unit; } - - &__remove { - flex-shrink: 0; - background: none; - border: none; - color: globals.$body-color; - cursor: pointer; - padding: globals.$spacing-unit; - border-radius: 50%; - transition: all ease 0.2s; - opacity: 0.5; - - &:hover { - opacity: 1; - color: globals.$error-color; - background-color: rgba(globals.$error-color, 0.1); - } - - &:disabled { - opacity: 0.3; - cursor: not-allowed; - } - } } diff --git a/src/renderer/src/pages/profile/profile-content/all-friends-modal.tsx b/src/renderer/src/pages/profile/profile-content/all-friends-modal.tsx index 735d88c1..4344956a 100644 --- a/src/renderer/src/pages/profile/profile-content/all-friends-modal.tsx +++ b/src/renderer/src/pages/profile/profile-content/all-friends-modal.tsx @@ -1,9 +1,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { XCircleIcon } from "@primer/octicons-react"; import { Modal, Avatar, Button } from "@renderer/components"; -import { useToast, useUserDetails } from "@renderer/hooks"; import { logger } from "@renderer/logger"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import type { UserFriend } from "@types"; @@ -26,15 +24,12 @@ export function AllFriendsModal({ }: AllFriendsModalProps) { const { t } = useTranslation("user_profile"); const navigate = useNavigate(); - const { undoFriendship } = useUserDetails(); - const { showSuccessToast, showErrorToast } = useToast(); const [friends, setFriends] = useState([]); const [totalFriends, setTotalFriends] = useState(0); const [isLoading, setIsLoading] = useState(false); const [hasMore, setHasMore] = useState(true); const [page, setPage] = useState(0); - const [removingId, setRemovingId] = useState(null); const listRef = useRef(null); const fetchFriends = useCallback( @@ -99,26 +94,6 @@ export function AllFriendsModal({ } }; - const handleRemoveFriend = useCallback( - async (e: React.MouseEvent, friendId: string) => { - e.stopPropagation(); - setRemovingId(friendId); - - try { - await undoFriendship(friendId); - setFriends((prev) => prev.filter((f) => f.id !== friendId)); - setTotalFriends((prev) => prev - 1); - showSuccessToast(t("friendship_removed")); - } catch (error) { - logger.error("Failed to remove friend", error); - showErrorToast(t("try_again")); - } finally { - setRemovingId(null); - } - }, - [undoFriendship, showSuccessToast, showErrorToast, t] - ); - const getGameImage = (game: { iconUrl: string | null; title: string }) => { if (game.iconUrl) { return {game.title}; @@ -177,17 +152,6 @@ export function AllFriendsModal({ )} - {isMe && ( - - )} ))} diff --git a/src/renderer/src/pages/profile/profile-content/friends-box.scss b/src/renderer/src/pages/profile/profile-content/friends-box.scss index 2b6d28b7..0bc76c31 100644 --- a/src/renderer/src/pages/profile/profile-content/friends-box.scss +++ b/src/renderer/src/pages/profile/profile-content/friends-box.scss @@ -13,6 +13,33 @@ border-radius: 4px; border: solid 1px globals.$border-color; padding: calc(globals.$spacing-unit * 2); + position: relative; + } + + &__add-friend-button { + background: none; + border: none; + color: globals.$body-color; + font-size: globals.$small-font-size; + cursor: pointer; + text-decoration: underline; + padding: 0; + transition: color ease 0.2s; + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit / 2); + + &:hover { + color: globals.$muted-color; + } + } + + &__view-all-container { + background-color: globals.$background-color; + padding-top: calc(globals.$spacing-unit * 2); + margin-top: calc(globals.$spacing-unit * 2); + display: flex; + justify-content: flex-start; } &__list { diff --git a/src/renderer/src/pages/profile/profile-content/friends-box.tsx b/src/renderer/src/pages/profile/profile-content/friends-box.tsx index dfa0fcc0..1f3f180a 100644 --- a/src/renderer/src/pages/profile/profile-content/friends-box.tsx +++ b/src/renderer/src/pages/profile/profile-content/friends-box.tsx @@ -2,9 +2,11 @@ import { userProfileContext } from "@renderer/context"; import { useFormat, useUserDetails } from "@renderer/hooks"; import { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; +import { PlusIcon } from "@primer/octicons-react"; import SteamLogo from "@renderer/assets/steam-logo.svg?react"; import { Avatar, Link } from "@renderer/components"; import { AllFriendsModal } from "./all-friends-modal"; +import { AddFriendModal } from "./add-friend-modal"; import "./friends-box.scss"; export function FriendsBox() { @@ -13,6 +15,7 @@ export function FriendsBox() { const { t } = useTranslation("user_profile"); const { numberFormatter } = useFormat(); const [showAllFriendsModal, setShowAllFriendsModal] = useState(false); + const [showAddFriendModal, setShowAddFriendModal] = useState(false); const isMe = userDetails?.id === userProfile?.id; @@ -45,13 +48,16 @@ export function FriendsBox() {
)} - + {isMe && ( + + )}
@@ -90,16 +96,31 @@ export function FriendsBox() { ))} +
+ +
{userProfile && ( - setShowAllFriendsModal(false)} - userId={userProfile.id} - isMe={isMe} - /> + <> + setShowAllFriendsModal(false)} + userId={userProfile.id} + isMe={isMe} + /> + setShowAddFriendModal(false)} + /> + )} ); diff --git a/src/renderer/src/pages/profile/profile-content/profile-content.tsx b/src/renderer/src/pages/profile/profile-content/profile-content.tsx index a5ae01da..c425b047 100644 --- a/src/renderer/src/pages/profile/profile-content/profile-content.tsx +++ b/src/renderer/src/pages/profile/profile-content/profile-content.tsx @@ -428,9 +428,9 @@ export function ProfileContent() { {shouldShowRightContent && (
+ -
diff --git a/src/renderer/src/pages/profile/profile-hero/profile-hero.scss b/src/renderer/src/pages/profile/profile-hero/profile-hero.scss index 6fd371c9..44abebc6 100644 --- a/src/renderer/src/pages/profile/profile-hero/profile-hero.scss +++ b/src/renderer/src/pages/profile/profile-hero/profile-hero.scss @@ -34,15 +34,15 @@ background: none; border: none; color: globals.$body-color; + background-color: rgba(0, 0, 0, 0.3); cursor: pointer; - padding: calc(globals.$spacing-unit / 2); - border-radius: 4px; + padding: calc(globals.$spacing-unit / 1.5); + border-radius: 6px; transition: all ease 0.2s; - opacity: 0.7; &:hover { opacity: 1; - background-color: rgba(255, 255, 255, 0.1); + background-color: rgba(0, 0, 0, 0.4); } } diff --git a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx index 6ae91cfd..d5259777 100644 --- a/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx +++ b/src/renderer/src/pages/shared-modals/user-friend-modal/user-friend-modal-list.tsx @@ -2,7 +2,7 @@ import type { UserFriend } from "@types"; import { useEffect, useRef, useState } from "react"; import { UserFriendItem } from "./user-friend-item"; import { useNavigate } from "react-router-dom"; -import { useToast, useUserDetails } from "@renderer/hooks"; +import { useUserDetails } from "@renderer/hooks"; import { useTranslation } from "react-i18next"; import Skeleton, { SkeletonTheme } from "react-loading-skeleton"; import "./user-friend-modal-list.scss"; @@ -19,7 +19,6 @@ export const UserFriendModalList = ({ closeModal, }: UserFriendModalListProps) => { const { t } = useTranslation("user_profile"); - const { showErrorToast } = useToast(); const navigate = useNavigate(); const [page, setPage] = useState(0); @@ -28,7 +27,7 @@ export const UserFriendModalList = ({ const [friends, setFriends] = useState([]); const listContainer = useRef(null); - const { userDetails, undoFriendship } = useUserDetails(); + const { userDetails } = useUserDetails(); const isMe = userDetails?.id == userId; const loadNextPage = () => { @@ -88,16 +87,6 @@ export const UserFriendModalList = ({ navigate(`/profile/${userId}`); }; - const handleUndoFriendship = (userId: string) => { - undoFriendship(userId) - .then(() => { - reloadList(); - }) - .catch(() => { - showErrorToast(t("try_again")); - }); - }; - return (
@@ -108,8 +97,7 @@ export const UserFriendModalList = ({ displayName={friend.displayName} profileImageUrl={friend.profileImageUrl} onClickItem={handleClickFriend} - onClickUndoFriendship={handleUndoFriendship} - type={isMe ? "ACCEPTED" : null} + type={null} key={"modal" + friend.id} /> ))}