feat: add fullscreen media modal to profile hero for avatar display

This commit is contained in:
Moyasee
2025-12-16 15:42:34 +02:00
parent 1552a5f359
commit affa7a2b2e
5 changed files with 169 additions and 4 deletions

2
proto

Submodule proto updated: 7a23620f93...6f11c99c57

View File

@@ -0,0 +1,61 @@
@use "../../scss/globals.scss";
.fullscreen-media-modal {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
position: relative;
background-color: rgba(0, 0, 0, 0.5);
&__close-button {
position: absolute;
top: calc(globals.$spacing-unit * 4);
right: calc(globals.$spacing-unit * 3);
cursor: pointer;
color: globals.$body-color;
background-color: rgba(0, 0, 0, 0.5);
border-radius: 50%;
border: 1px solid globals.$border-color;
padding: globals.$spacing-unit;
display: flex;
align-items: center;
justify-content: center;
transition: all ease 0.2s;
z-index: 10;
&:hover {
background-color: rgba(0, 0, 0, 0.8);
transform: scale(1.1);
}
}
&__image-container {
max-width: 90%;
max-height: 90%;
display: flex;
justify-content: center;
align-items: center;
}
&__image {
max-width: 100%;
max-height: 60vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: image-appear 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
}
@keyframes image-appear {
0% {
opacity: 0;
transform: scale(0.85);
}
100% {
opacity: 1;
transform: scale(1);
}
}

View File

@@ -0,0 +1,88 @@
import { useCallback, useEffect, useRef } from "react";
import { createPortal } from "react-dom";
import { XIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import { Backdrop } from "../backdrop/backdrop";
import "./fullscreen-media-modal.scss";
export interface FullscreenMediaModalProps {
visible: boolean;
onClose: () => void;
src: string | null | undefined;
alt?: string;
}
export function FullscreenMediaModal({
visible,
onClose,
src,
alt,
}: FullscreenMediaModalProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const { t } = useTranslation("modal");
useEffect(() => {
if (visible) {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}
return () => {};
}, [onClose, visible]);
useEffect(() => {
const onMouseDown = (e: MouseEvent) => {
if (containerRef.current) {
const clickedOnImage = containerRef.current.contains(e.target as Node);
if (!clickedOnImage) {
onClose();
}
}
};
if (visible) {
window.addEventListener("mousedown", onMouseDown);
}
return () => {
window.removeEventListener("mousedown", onMouseDown);
};
}, [onClose, visible]);
if (!visible || !src) return null;
return createPortal(
<Backdrop>
<div className="fullscreen-media-modal" role="dialog" aria-label={alt}>
<button
type="button"
onClick={onClose}
className="fullscreen-media-modal__close-button"
aria-label={t("close")}
>
<XIcon size={24} />
</button>
<div
ref={containerRef}
className="fullscreen-media-modal__image-container"
>
<img src={src} alt={alt} className="fullscreen-media-modal__image" />
</div>
</div>
</Backdrop>,
document.body
);
}

View File

@@ -20,3 +20,4 @@ export * from "./game-context-menu/game-context-menu";
export * from "./game-context-menu/use-game-actions"; export * from "./game-context-menu/use-game-actions";
export * from "./star-rating/star-rating"; export * from "./star-rating/star-rating";
export * from "./search-dropdown/search-dropdown"; export * from "./search-dropdown/search-dropdown";
export * from "./fullscreen-media-modal/fullscreen-media-modal";

View File

@@ -9,7 +9,12 @@ import {
XCircleFillIcon, XCircleFillIcon,
} from "@primer/octicons-react"; } from "@primer/octicons-react";
import { buildGameDetailsPath } from "@renderer/helpers"; import { buildGameDetailsPath } from "@renderer/helpers";
import { Avatar, Button, Link } from "@renderer/components"; import {
Avatar,
Button,
FullscreenMediaModal,
Link,
} from "@renderer/components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
useAppSelector, useAppSelector,
@@ -33,6 +38,7 @@ type FriendAction =
export function ProfileHero() { export function ProfileHero() {
const [showEditProfileModal, setShowEditProfileModal] = useState(false); const [showEditProfileModal, setShowEditProfileModal] = useState(false);
const [showFullscreenAvatar, setShowFullscreenAvatar] = useState(false);
const [isPerformingAction, setIsPerformingAction] = useState(false); const [isPerformingAction, setIsPerformingAction] = useState(false);
const { const {
@@ -246,10 +252,12 @@ export function ProfileHero() {
]); ]);
const handleAvatarClick = useCallback(() => { const handleAvatarClick = useCallback(() => {
if (isMe) { if (userProfile?.profileImageUrl) {
setShowFullscreenAvatar(true);
} else if (isMe) {
setShowEditProfileModal(true); setShowEditProfileModal(true);
} }
}, [isMe]); }, [isMe, userProfile?.profileImageUrl]);
const currentGame = useMemo(() => { const currentGame = useMemo(() => {
if (isMe) { if (isMe) {
@@ -272,6 +280,13 @@ export function ProfileHero() {
onClose={() => setShowEditProfileModal(false)} onClose={() => setShowEditProfileModal(false)}
/> />
<FullscreenMediaModal
visible={showFullscreenAvatar}
onClose={() => setShowFullscreenAvatar(false)}
src={userProfile?.profileImageUrl}
alt={userProfile?.displayName}
/>
<section <section
className="profile-hero__content-box" className="profile-hero__content-box"
style={{ background: !backgroundImage ? heroBackground : undefined }} style={{ background: !backgroundImage ? heroBackground : undefined }}