mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-02-01 07:11:02 +00:00
fix: merge conflict
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
min-width: 200px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
animation: dropdown-menu-fade-in 0.2s ease-out;
|
||||
}
|
||||
|
||||
&__group {
|
||||
@@ -66,3 +67,14 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dropdown-menu-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,21 +224,6 @@ export function Header() {
|
||||
setActiveIndex(-1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const prevPath = sessionStorage.getItem("prevPath");
|
||||
const currentPath = location.pathname;
|
||||
|
||||
if (
|
||||
prevPath?.startsWith("/catalogue") &&
|
||||
!currentPath.startsWith("/catalogue") &&
|
||||
catalogueSearchValue
|
||||
) {
|
||||
dispatch(setFilters({ title: "" }));
|
||||
}
|
||||
|
||||
sessionStorage.setItem("prevPath", currentPath);
|
||||
}, [location.pathname, catalogueSearchValue, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDropdownVisible) return;
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ export const DOWNLOADER_NAME = {
|
||||
[Downloader.Torrent]: "Torrent",
|
||||
[Downloader.Gofile]: "Gofile",
|
||||
[Downloader.PixelDrain]: "PixelDrain",
|
||||
[Downloader.Qiwi]: "Qiwi",
|
||||
[Downloader.Datanodes]: "Datanodes",
|
||||
[Downloader.Mediafire]: "Mediafire",
|
||||
[Downloader.Buzzheavier]: "Buzzheavier",
|
||||
@@ -15,6 +14,7 @@ export const DOWNLOADER_NAME = {
|
||||
[Downloader.TorBox]: "TorBox",
|
||||
[Downloader.Hydra]: "Nimbus",
|
||||
[Downloader.VikingFile]: "VikingFile",
|
||||
[Downloader.Rootz]: "Rootz",
|
||||
};
|
||||
|
||||
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;
|
||||
|
||||
4
src/renderer/src/declaration.d.ts
vendored
4
src/renderer/src/declaration.d.ts
vendored
@@ -167,6 +167,10 @@ declare global {
|
||||
getLibrary: () => Promise<LibraryGame[]>;
|
||||
refreshLibraryAssets: () => Promise<void>;
|
||||
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
|
||||
getGameInstallerActionType: (
|
||||
shop: GameShop,
|
||||
objectId: string
|
||||
) => Promise<"install" | "open-folder">;
|
||||
openGameInstallerPath: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
openGameExecutablePath: (shop: GameShop, objectId: string) => Promise<void>;
|
||||
openGame: (
|
||||
|
||||
@@ -511,6 +511,13 @@
|
||||
min-height: unset;
|
||||
}
|
||||
|
||||
&__simple-action-btn {
|
||||
padding: calc(globals.$spacing-unit);
|
||||
min-height: unset;
|
||||
gap: calc(globals.$spacing-unit);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
&__progress-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -32,12 +32,12 @@ import {
|
||||
FileDirectoryIcon,
|
||||
LinkIcon,
|
||||
PlayIcon,
|
||||
ThreeBarsIcon,
|
||||
TrashIcon,
|
||||
UnlinkIcon,
|
||||
XCircleIcon,
|
||||
GraphIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { MoreVertical, Folder } from "lucide-react";
|
||||
import { average } from "color.js";
|
||||
|
||||
interface AnimatedPercentageProps {
|
||||
@@ -452,6 +452,7 @@ export function DownloadGroup({
|
||||
seedingStatus,
|
||||
}: Readonly<DownloadGroupProps>) {
|
||||
const { t } = useTranslation("downloads");
|
||||
const { t: tGameDetails } = useTranslation("game_details");
|
||||
const navigate = useNavigate();
|
||||
|
||||
const userPreferences = useAppSelector(
|
||||
@@ -530,6 +531,9 @@ export function DownloadGroup({
|
||||
const [gameToCancelObjectId, setGameToCancelObjectId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [gameActionTypes, setGameActionTypes] = useState<
|
||||
Record<string, "install" | "open-folder">
|
||||
>({});
|
||||
|
||||
const extractDominantColor = useCallback(
|
||||
async (imageUrl: string, gameId: string) => {
|
||||
@@ -805,6 +809,37 @@ export function DownloadGroup({
|
||||
]
|
||||
);
|
||||
|
||||
// Fetch action types for completed games
|
||||
useEffect(() => {
|
||||
const fetchActionTypes = async () => {
|
||||
const completedGames = library.filter(
|
||||
(game) => game.download?.progress === 1
|
||||
);
|
||||
|
||||
const actionTypesPromises = completedGames.map(async (game) => {
|
||||
try {
|
||||
const actionType = await window.electron.getGameInstallerActionType(
|
||||
game.shop,
|
||||
game.objectId
|
||||
);
|
||||
return { gameId: game.id, actionType };
|
||||
} catch {
|
||||
return { gameId: game.id, actionType: "open-folder" as const };
|
||||
}
|
||||
});
|
||||
|
||||
const results = await Promise.all(actionTypesPromises);
|
||||
const newActionTypes: Record<string, "install" | "open-folder"> = {};
|
||||
results.forEach(({ gameId, actionType }) => {
|
||||
newActionTypes[gameId] = actionType;
|
||||
});
|
||||
|
||||
setGameActionTypes((prev) => ({ ...prev, ...newActionTypes }));
|
||||
};
|
||||
|
||||
fetchActionTypes();
|
||||
}, [library]);
|
||||
|
||||
if (!library.length) return null;
|
||||
|
||||
const isDownloadingGroup = title === t("download_in_progress");
|
||||
@@ -959,18 +994,35 @@ export function DownloadGroup({
|
||||
)}
|
||||
|
||||
<div className="download-group__simple-actions">
|
||||
{game.download?.progress === 1 && (
|
||||
<Button
|
||||
theme="primary"
|
||||
onClick={() =>
|
||||
openGameInstaller(game.shop, game.objectId)
|
||||
}
|
||||
disabled={isGameDeleting(game.id)}
|
||||
className="download-group__simple-menu-btn"
|
||||
>
|
||||
<PlayIcon size={16} />
|
||||
</Button>
|
||||
)}
|
||||
{game.download?.progress === 1 &&
|
||||
(() => {
|
||||
const actionType =
|
||||
gameActionTypes[game.id] || "open-folder";
|
||||
const isInstall = actionType === "install";
|
||||
|
||||
return (
|
||||
<Button
|
||||
theme="primary"
|
||||
onClick={() =>
|
||||
openGameInstaller(game.shop, game.objectId)
|
||||
}
|
||||
disabled={isGameDeleting(game.id)}
|
||||
className="download-group__simple-action-btn"
|
||||
>
|
||||
{isInstall ? (
|
||||
<>
|
||||
<DownloadIcon size={16} />
|
||||
{t("install")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Folder size={16} />
|
||||
{tGameDetails("open_folder")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
})()}
|
||||
{isQueuedGroup && game.download?.progress !== 1 && (
|
||||
<Button
|
||||
theme="primary"
|
||||
@@ -986,7 +1038,7 @@ export function DownloadGroup({
|
||||
theme="outline"
|
||||
className="download-group__simple-menu-btn"
|
||||
>
|
||||
<ThreeBarsIcon />
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&--wrapped {
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__list-title {
|
||||
@@ -70,4 +76,15 @@
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
&__wrapped-link {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
text-align: start;
|
||||
color: globals.$body-color;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useContext } from "react";
|
||||
import { useCallback, useContext, useState } from "react";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFormat, useUserDetails } from "@renderer/hooks";
|
||||
@@ -7,9 +7,11 @@ import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
|
||||
import { useSubscription } from "@renderer/hooks/use-subscription";
|
||||
import { ClockIcon, TrophyIcon } from "@primer/octicons-react";
|
||||
import { Award } from "lucide-react";
|
||||
import { WrappedFullscreenModal } from "./wrapped-tab";
|
||||
import "./user-stats-box.scss";
|
||||
|
||||
export function UserStatsBox() {
|
||||
const [showWrappedModal, setShowWrappedModal] = useState(false);
|
||||
const { showHydraCloudModal } = useSubscription();
|
||||
const { userStats, isMe, userProfile } = useContext(userProfileContext);
|
||||
const { userDetails } = useUserDetails();
|
||||
@@ -41,6 +43,18 @@ export function UserStatsBox() {
|
||||
return (
|
||||
<div className="user-stats__box">
|
||||
<ul className="user-stats__list">
|
||||
{userProfile?.hasCompletedWrapped2025 && (
|
||||
<li className="user-stats__list-item user-stats__list-item--wrapped">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowWrappedModal(true)}
|
||||
className="user-stats__wrapped-link"
|
||||
>
|
||||
Wrapped 2025
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
|
||||
{(isMe || userStats.unlockedAchievementSum !== undefined) && (
|
||||
<li className="user-stats__list-item">
|
||||
<h3 className="user-stats__list-title">
|
||||
@@ -126,6 +140,14 @@ export function UserStatsBox() {
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
|
||||
{userProfile && (
|
||||
<WrappedFullscreenModal
|
||||
userId={userProfile.id}
|
||||
isOpen={showWrappedModal}
|
||||
onClose={() => setShowWrappedModal(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,11 +144,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__left-actions {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: globals.$spacing-unit;
|
||||
@@ -160,35 +155,5 @@
|
||||
&--outline {
|
||||
border-color: globals.$body-color;
|
||||
}
|
||||
|
||||
&--wrapped {
|
||||
background: linear-gradient(
|
||||
120deg,
|
||||
#2a57ff 0%,
|
||||
#2951e6 11%,
|
||||
#2f5bff 16%,
|
||||
#2c56e8 29%,
|
||||
#244acc 34%,
|
||||
#2245c2 40%,
|
||||
#3a6bff 45%,
|
||||
#3766f2 50%,
|
||||
#2444b8 56%,
|
||||
#122a73 82%,
|
||||
#2348b3 86%,
|
||||
#1f429e 87%,
|
||||
#10286a 93%,
|
||||
#0e2a63 100%
|
||||
);
|
||||
background-color: #2a57ff;
|
||||
background-size: 105% 100%;
|
||||
background-position: 100% 50%;
|
||||
border: none;
|
||||
color: white;
|
||||
transition: background-position 0.4s ease;
|
||||
|
||||
&:hover {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
PencilIcon,
|
||||
PersonAddIcon,
|
||||
SignOutIcon,
|
||||
TrophyIcon,
|
||||
XCircleFillIcon,
|
||||
} from "@primer/octicons-react";
|
||||
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||
@@ -30,7 +29,6 @@ import { motion } from "framer-motion";
|
||||
|
||||
import type { FriendRequestAction } from "@types";
|
||||
import { EditProfileModal } from "../edit-profile-modal/edit-profile-modal";
|
||||
import { WrappedFullscreenModal } from "../profile-content/wrapped-tab";
|
||||
import Skeleton from "react-loading-skeleton";
|
||||
import { UploadBackgroundImageButton } from "../upload-background-image-button/upload-background-image-button";
|
||||
import "./profile-hero.scss";
|
||||
@@ -41,10 +39,10 @@ type FriendAction =
|
||||
|
||||
export function ProfileHero() {
|
||||
const [showEditProfileModal, setShowEditProfileModal] = useState(false);
|
||||
const [showWrappedModal, setShowWrappedModal] = useState(false);
|
||||
const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false);
|
||||
const [isPerformingAction, setIsPerformingAction] = useState(false);
|
||||
const [isCopyButtonHovered, setIsCopyButtonHovered] = useState(false);
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const { isMe, getUserProfile, userProfile, heroBackground, backgroundImage } =
|
||||
useContext(userProfileContext);
|
||||
@@ -261,9 +259,23 @@ export function ProfileHero() {
|
||||
const copyFriendCode = useCallback(() => {
|
||||
if (userProfile?.id) {
|
||||
navigator.clipboard.writeText(userProfile.id);
|
||||
showSuccessToast(t("friend_code_copied"));
|
||||
setIsCopied(true);
|
||||
|
||||
const startTime = performance.now();
|
||||
const duration = 1200; // 1.2 seconds
|
||||
|
||||
const animate = (currentTime: number) => {
|
||||
const elapsed = currentTime - startTime;
|
||||
if (elapsed < duration) {
|
||||
requestAnimationFrame(animate);
|
||||
} else {
|
||||
setIsCopied(false);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
}, [userProfile, showSuccessToast, t]);
|
||||
}, [userProfile]);
|
||||
|
||||
const currentGame = useMemo(() => {
|
||||
if (isMe) {
|
||||
@@ -286,13 +298,6 @@ export function ProfileHero() {
|
||||
onClose={() => setShowEditProfileModal(false)}
|
||||
/>
|
||||
|
||||
{userProfile && (
|
||||
<WrappedFullscreenModal
|
||||
userId={userProfile.id}
|
||||
isOpen={showWrappedModal}
|
||||
onClose={() => setShowWrappedModal(false)}
|
||||
/>
|
||||
)}
|
||||
<FullscreenMediaModal
|
||||
visible={showFullscreenAvatar}
|
||||
onClose={() => setShowFullscreenAvatar(false)}
|
||||
@@ -348,7 +353,7 @@ export function ProfileHero() {
|
||||
onMouseLeave={() => setIsCopyButtonHovered(false)}
|
||||
initial={{ width: 28 }}
|
||||
animate={{
|
||||
width: isCopyButtonHovered ? 105 : 28,
|
||||
width: isCopyButtonHovered || isCopied ? 105 : 28,
|
||||
}}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
>
|
||||
@@ -356,12 +361,12 @@ export function ProfileHero() {
|
||||
className="profile-hero__friend-code"
|
||||
initial={{ opacity: 0, marginRight: 0 }}
|
||||
animate={{
|
||||
opacity: isCopyButtonHovered ? 1 : 0,
|
||||
marginRight: isCopyButtonHovered ? 8 : 0,
|
||||
opacity: isCopyButtonHovered || isCopied ? 1 : 0,
|
||||
marginRight: isCopyButtonHovered || isCopied ? 8 : 0,
|
||||
}}
|
||||
transition={{ duration: 0.2, ease: "easeInOut" }}
|
||||
>
|
||||
{userProfile?.id}
|
||||
{isCopied ? t("copied") : userProfile?.id}
|
||||
</motion.span>
|
||||
<CopyIcon size={16} />
|
||||
</motion.button>
|
||||
@@ -410,22 +415,6 @@ export function ProfileHero() {
|
||||
background: !backgroundImage ? heroBackground : undefined,
|
||||
}}
|
||||
>
|
||||
{userProfile?.hasCompletedWrapped2025 && (
|
||||
<div className="profile-hero__left-actions">
|
||||
<Button
|
||||
theme="outline"
|
||||
onClick={() => setShowWrappedModal(true)}
|
||||
className="profile-hero__button--wrapped"
|
||||
>
|
||||
<TrophyIcon />
|
||||
{isMe
|
||||
? t("view_my_wrapped_button")
|
||||
: t("view_wrapped_button", {
|
||||
displayName: userProfile.displayName,
|
||||
})}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="profile-hero__actions">{profileActions}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,11 +1,86 @@
|
||||
@use "../../../scss/globals.scss";
|
||||
|
||||
.upload-background-image-button {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
&__wrapper {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
border-color: globals.$body-color;
|
||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.8);
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(20px);
|
||||
|
||||
&__menu {
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 6px;
|
||||
min-width: 180px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
|
||||
z-index: 1000;
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
animation: menu-fade-in 0.2s ease-out;
|
||||
|
||||
&--closing {
|
||||
animation: menu-fade-out 0.15s ease-in;
|
||||
}
|
||||
}
|
||||
|
||||
&__menu-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
border-radius: 4px;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
font-size: 14px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: globals.$body-color;
|
||||
text-align: left;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menu-fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes menu-fade-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { UploadIcon } from "@primer/octicons-react";
|
||||
import { Button } from "@renderer/components";
|
||||
import { useContext, useState } from "react";
|
||||
import { TrashIcon, UploadIcon } from "@primer/octicons-react";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
import { Button, ConfirmationModal } from "@renderer/components";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useContext, useEffect, useRef, useState } from "react";
|
||||
import { userProfileContext } from "@renderer/context";
|
||||
import { useToast, useUserDetails } from "@renderer/hooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -9,16 +11,33 @@ import "./upload-background-image-button.scss";
|
||||
export function UploadBackgroundImageButton() {
|
||||
const [isUploadingBackgroundImage, setIsUploadingBackgorundImage] =
|
||||
useState(false);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [isMenuClosing, setIsMenuClosing] = useState(false);
|
||||
const [showRemoveBannerModal, setShowRemoveBannerModal] = useState(false);
|
||||
const buttonRef = useRef<HTMLDivElement>(null);
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const { hasActiveSubscription } = useUserDetails();
|
||||
|
||||
const { t } = useTranslation("user_profile");
|
||||
|
||||
const { isMe, setSelectedBackgroundImage } = useContext(userProfileContext);
|
||||
const { isMe, setSelectedBackgroundImage, userProfile, getUserProfile } =
|
||||
useContext(userProfileContext);
|
||||
const { patchUser, fetchUserDetails } = useUserDetails();
|
||||
|
||||
const { showSuccessToast } = useToast();
|
||||
|
||||
const handleChangeCoverClick = async () => {
|
||||
const hasBanner = !!userProfile?.backgroundImageUrl;
|
||||
|
||||
const closeMenu = () => {
|
||||
setIsMenuClosing(true);
|
||||
setTimeout(() => {
|
||||
setIsMenuOpen(false);
|
||||
setIsMenuClosing(false);
|
||||
}, 150);
|
||||
};
|
||||
|
||||
const handleReplaceBanner = async () => {
|
||||
closeMenu();
|
||||
try {
|
||||
const { filePaths } = await window.electron.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
@@ -40,23 +59,159 @@ export function UploadBackgroundImageButton() {
|
||||
|
||||
showSuccessToast(t("background_image_updated"));
|
||||
await fetchUserDetails();
|
||||
await getUserProfile();
|
||||
}
|
||||
} finally {
|
||||
setIsUploadingBackgorundImage(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveBannerClick = () => {
|
||||
closeMenu();
|
||||
setShowRemoveBannerModal(true);
|
||||
};
|
||||
|
||||
const handleRemoveBannerConfirm = async () => {
|
||||
setShowRemoveBannerModal(false);
|
||||
try {
|
||||
setIsUploadingBackgorundImage(true);
|
||||
setSelectedBackgroundImage("");
|
||||
await patchUser({ backgroundImageUrl: null });
|
||||
showSuccessToast(t("background_image_updated"));
|
||||
await fetchUserDetails();
|
||||
await getUserProfile();
|
||||
} finally {
|
||||
setIsUploadingBackgorundImage(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle click outside, scroll, and escape key to close menu
|
||||
useEffect(() => {
|
||||
if (!isMenuOpen) return;
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target as Node;
|
||||
if (
|
||||
menuRef.current &&
|
||||
!menuRef.current.contains(target) &&
|
||||
buttonRef.current &&
|
||||
!buttonRef.current.contains(target)
|
||||
) {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleScroll = () => {
|
||||
closeMenu();
|
||||
};
|
||||
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
document.addEventListener("keydown", handleEscape);
|
||||
window.addEventListener("scroll", handleScroll, true);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
document.removeEventListener("keydown", handleEscape);
|
||||
window.removeEventListener("scroll", handleScroll, true);
|
||||
};
|
||||
}, [isMenuOpen]);
|
||||
|
||||
if (!isMe || !hasActiveSubscription) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
theme="outline"
|
||||
className="upload-background-image-button"
|
||||
onClick={handleChangeCoverClick}
|
||||
disabled={isUploadingBackgroundImage}
|
||||
// If no banner exists, show the original upload button
|
||||
if (!hasBanner) {
|
||||
return (
|
||||
<div className="upload-background-image-button__wrapper">
|
||||
<Button
|
||||
theme="outline"
|
||||
className="upload-background-image-button"
|
||||
onClick={handleReplaceBanner}
|
||||
disabled={isUploadingBackgroundImage}
|
||||
>
|
||||
<UploadIcon />
|
||||
{isUploadingBackgroundImage
|
||||
? t("uploading_banner")
|
||||
: t("upload_banner")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate menu position
|
||||
const getMenuPosition = () => {
|
||||
if (!buttonRef.current) return { top: 0, right: 0 };
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
return {
|
||||
top: rect.bottom + 5,
|
||||
right: window.innerWidth - rect.right,
|
||||
};
|
||||
};
|
||||
|
||||
const menuPosition = isMenuOpen ? getMenuPosition() : { top: 0, right: 0 };
|
||||
|
||||
const menuContent = isMenuOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
className={`upload-background-image-button__menu ${
|
||||
isMenuClosing ? "upload-background-image-button__menu--closing" : ""
|
||||
}`}
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: `${menuPosition.top}px`,
|
||||
right: `${menuPosition.right}px`,
|
||||
}}
|
||||
>
|
||||
<UploadIcon />
|
||||
{isUploadingBackgroundImage ? t("uploading_banner") : t("upload_banner")}
|
||||
</Button>
|
||||
<button
|
||||
type="button"
|
||||
className="upload-background-image-button__menu-item"
|
||||
onClick={handleReplaceBanner}
|
||||
disabled={isUploadingBackgroundImage}
|
||||
>
|
||||
<UploadIcon size={16} />
|
||||
{t("replace_banner")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="upload-background-image-button__menu-item"
|
||||
onClick={handleRemoveBannerClick}
|
||||
disabled={isUploadingBackgroundImage}
|
||||
>
|
||||
<TrashIcon size={16} />
|
||||
{t("remove_banner")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={buttonRef} className="upload-background-image-button__wrapper">
|
||||
<Button
|
||||
theme="outline"
|
||||
className="upload-background-image-button"
|
||||
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||
disabled={isUploadingBackgroundImage}
|
||||
>
|
||||
{t("change_banner")}
|
||||
<MoreVertical size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
{createPortal(menuContent, document.body)}
|
||||
<ConfirmationModal
|
||||
visible={showRemoveBannerModal}
|
||||
title={t("remove_banner_modal_title")}
|
||||
descriptionText={t("remove_banner_confirmation")}
|
||||
onClose={() => setShowRemoveBannerModal(false)}
|
||||
onConfirm={handleRemoveBannerConfirm}
|
||||
cancelButtonLabel={t("cancel")}
|
||||
confirmButtonLabel={t("remove")}
|
||||
buttonsIsDisabled={isUploadingBackgroundImage}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user