mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-29 05:41:03 +00:00
ci: moving souvenirs into separate tab
This commit is contained in:
@@ -442,7 +442,9 @@
|
|||||||
border-color: rgba(244, 67, 54, 0.4);
|
border-color: rgba(244, 67, 54, 0.4);
|
||||||
color: #ff7961;
|
color: #ff7961;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-content {
|
||||||
&__images-section {
|
&__images-section {
|
||||||
margin-bottom: calc(globals.$spacing-unit * 3);
|
margin-bottom: calc(globals.$spacing-unit * 3);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ import {
|
|||||||
import { ProfileHero } from "../profile-hero/profile-hero";
|
import { ProfileHero } from "../profile-hero/profile-hero";
|
||||||
import { useAppDispatch, useFormat, useUserDetails } from "@renderer/hooks";
|
import { useAppDispatch, useFormat, useUserDetails } from "@renderer/hooks";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
import { setHeaderTitle } from "@renderer/features";
|
||||||
import {
|
import { TelescopeIcon } from "@primer/octicons-react";
|
||||||
TelescopeIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
SearchIcon,
|
|
||||||
} from "@primer/octicons-react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { GameShop } from "@types";
|
import type { GameShop } from "@types";
|
||||||
import { LockedProfile } from "./locked-profile";
|
import { LockedProfile } from "./locked-profile";
|
||||||
@@ -23,23 +19,16 @@ import { FriendsBox } 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 { UserLibraryGameCard } from "./user-library-game-card";
|
import { logger } from "@renderer/logger";
|
||||||
import { SortOptions } from "./sort-options";
|
import { AnimatePresence } from "framer-motion";
|
||||||
import { useSectionCollapse } from "@renderer/hooks/use-section-collapse";
|
import { GAME_STATS_ANIMATION_DURATION_IN_MS } from "./profile-animations";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
|
||||||
import {
|
|
||||||
sectionVariants,
|
|
||||||
chevronVariants,
|
|
||||||
GAME_STATS_ANIMATION_DURATION_IN_MS,
|
|
||||||
} from "./profile-animations";
|
|
||||||
import { FullscreenImageModal } from "@renderer/components/fullscreen-image-modal";
|
import { FullscreenImageModal } from "@renderer/components/fullscreen-image-modal";
|
||||||
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 { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
|
||||||
import { ProfileTabs } from "./profile-tabs";
|
import { ProfileTabs } from "./profile-tabs";
|
||||||
import { LibraryTab } from "./library-tab";
|
import { LibraryTab } from "./library-tab";
|
||||||
import { ReviewsTab } from "./reviews-tab";
|
import { ReviewsTab } from "./reviews-tab";
|
||||||
import { AnimatePresence } from "framer-motion";
|
import { SouvenirsTab } from "./souvenirs-tab";
|
||||||
import "./profile-content.scss";
|
import "./profile-content.scss";
|
||||||
|
|
||||||
type SortOption = "playtime" | "achievementCount" | "playedRecently";
|
type SortOption = "playtime" | "achievementCount" | "playedRecently";
|
||||||
@@ -114,7 +103,9 @@ export function ProfileContent() {
|
|||||||
} | null>(null);
|
} | null>(null);
|
||||||
const statsAnimation = useRef(-1);
|
const statsAnimation = useRef(-1);
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<"library" | "reviews">("library");
|
const [activeTab, setActiveTab] = useState<
|
||||||
|
"library" | "reviews" | "souvenirs"
|
||||||
|
>("library");
|
||||||
|
|
||||||
// User reviews state
|
// User reviews state
|
||||||
const [reviews, setReviews] = useState<UserReview[]>([]);
|
const [reviews, setReviews] = useState<UserReview[]>([]);
|
||||||
@@ -226,7 +217,7 @@ export function ProfileContent() {
|
|||||||
setReviews((prev) => prev.filter((review) => review.id !== reviewId));
|
setReviews((prev) => prev.filter((review) => review.id !== reviewId));
|
||||||
setReviewsTotalCount((prev) => prev - 1);
|
setReviewsTotalCount((prev) => prev - 1);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete review:", error);
|
logger.error("Failed to delete review:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -321,7 +312,7 @@ export function ProfileContent() {
|
|||||||
`/games/${review.game.shop}/${review.game.objectId}/reviews/${reviewId}/${endpoint}`
|
`/games/${review.game.shop}/${review.game.objectId}/reviews/${reviewId}/${endpoint}`
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to vote on review:", error);
|
logger.error("Failed to vote on review:", error);
|
||||||
|
|
||||||
// Rollback optimistic update on error
|
// Rollback optimistic update on error
|
||||||
setReviews((prev) =>
|
setReviews((prev) =>
|
||||||
@@ -424,222 +415,56 @@ export function ProfileContent() {
|
|||||||
|
|
||||||
{hasAnyGames && (
|
{hasAnyGames && (
|
||||||
<div>
|
<div>
|
||||||
{hasPinnedGames && (
|
<ProfileTabs
|
||||||
<div style={{ marginBottom: "2rem" }}>
|
activeTab={activeTab}
|
||||||
<div className="profile-content__section-header">
|
reviewsTotalCount={reviewsTotalCount}
|
||||||
<div className="profile-content__section-title-group">
|
souvenirsCount={userProfile?.achievements?.length || 0}
|
||||||
<button
|
onTabChange={setActiveTab}
|
||||||
type="button"
|
/>
|
||||||
className="profile-content__collapse-button"
|
|
||||||
onClick={() => toggleSection("pinned")}
|
|
||||||
aria-label={
|
|
||||||
isPinnedCollapsed
|
|
||||||
? "Expand pinned section"
|
|
||||||
: "Collapse pinned section"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<motion.div
|
|
||||||
variants={chevronVariants}
|
|
||||||
animate={isPinnedCollapsed ? "collapsed" : "expanded"}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon size={16} />
|
|
||||||
</motion.div>
|
|
||||||
</button>
|
|
||||||
<h2>{t("pinned")}</h2>
|
|
||||||
<span className="profile-content__section-badge">
|
|
||||||
{pinnedGames.length}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AnimatePresence initial={true} mode="wait">
|
<div className="profile-content__tab-panels">
|
||||||
{!isPinnedCollapsed && (
|
<AnimatePresence mode="wait">
|
||||||
<motion.div
|
{activeTab === "library" && (
|
||||||
key="pinned-content"
|
<LibraryTab
|
||||||
variants={sectionVariants}
|
sortBy={sortBy}
|
||||||
initial="collapsed"
|
onSortChange={setSortBy}
|
||||||
animate="expanded"
|
pinnedGames={pinnedGames}
|
||||||
exit="collapsed"
|
libraryGames={libraryGames}
|
||||||
layout
|
hasMoreLibraryGames={hasMoreLibraryGames}
|
||||||
>
|
isLoadingLibraryGames={isLoadingLibraryGames}
|
||||||
<ul className="profile-content__games-grid">
|
statsIndex={statsIndex}
|
||||||
{pinnedGames?.map((game) => (
|
userStats={userStats}
|
||||||
<li
|
animatedGameIdsRef={animatedGameIdsRef}
|
||||||
key={game.objectId}
|
onLoadMore={handleLoadMore}
|
||||||
style={{ listStyle: "none" }}
|
onMouseEnter={handleOnMouseEnterGameCard}
|
||||||
>
|
onMouseLeave={handleOnMouseLeaveGameCard}
|
||||||
<UserLibraryGameCard
|
isMe={isMe}
|
||||||
game={game}
|
/>
|
||||||
statIndex={statsIndex}
|
)}
|
||||||
onMouseEnter={handleOnMouseEnterGameCard}
|
|
||||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
|
||||||
sortBy={sortBy}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{userProfile?.achievements &&
|
{activeTab === "reviews" && (
|
||||||
userProfile.achievements.length > 0 && (
|
<ReviewsTab
|
||||||
<div className="profile-content__images-section">
|
reviews={reviews}
|
||||||
<div className="profile-content__section-header">
|
isLoadingReviews={isLoadingReviews}
|
||||||
<div className="profile-content__section-title-group">
|
votingReviews={votingReviews}
|
||||||
<h2>{t("souvenirs")}</h2>
|
userDetailsId={userDetails?.id}
|
||||||
<span className="profile-content__section-badge">
|
formatPlayTime={formatPlayTime}
|
||||||
{userProfile.achievements.length}
|
getRatingText={getRatingText}
|
||||||
</span>
|
onVote={handleVoteReview}
|
||||||
</div>
|
onDelete={handleDeleteClick}
|
||||||
</div>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="profile-content__images-grid">
|
{activeTab === "souvenirs" && (
|
||||||
{userProfile.achievements.map((achievement, index) => (
|
<SouvenirsTab
|
||||||
<div
|
achievements={userProfile?.achievements || []}
|
||||||
key={`${achievement.gameTitle}-${achievement.name}-${index}`}
|
onImageClick={handleImageClick}
|
||||||
className="profile-content__image-card"
|
/>
|
||||||
>
|
)}
|
||||||
<div className="profile-content__image-card-header">
|
</AnimatePresence>
|
||||||
<div className="profile-content__image-achievement-image-wrapper">
|
</div>
|
||||||
<button
|
</div>
|
||||||
type="button"
|
)}
|
||||||
className="profile-content__image-button"
|
|
||||||
onClick={() =>
|
|
||||||
handleImageClick(
|
|
||||||
achievement.imageUrl,
|
|
||||||
achievement.name
|
|
||||||
)
|
|
||||||
}
|
|
||||||
aria-label={`View ${achievement.name} screenshot in fullscreen`}
|
|
||||||
style={{
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: 0,
|
|
||||||
border: "none",
|
|
||||||
background: "transparent",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={achievement.imageUrl}
|
|
||||||
alt={achievement.name}
|
|
||||||
className="profile-content__image-achievement-image"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
<div className="profile-content__image-achievement-image-overlay">
|
|
||||||
<SearchIcon size={20} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="profile-content__image-card-content">
|
|
||||||
<div className="profile-content__image-achievement-info">
|
|
||||||
<img
|
|
||||||
src={achievement.achievementIcon}
|
|
||||||
alt=""
|
|
||||||
className="profile-content__image-achievement-icon"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<span className="profile-content__image-achievement-name">
|
|
||||||
{achievement.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="profile-content__image-game-info">
|
|
||||||
<div className="profile-content__image-game-left">
|
|
||||||
<img
|
|
||||||
src={achievement.gameIconUrl}
|
|
||||||
alt=""
|
|
||||||
className="profile-content__image-game-icon"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
<span className="profile-content__image-game-title">
|
|
||||||
{achievement.gameTitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="profile-content__image-card-gradient-overlay"></div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasAnyGames && (
|
|
||||||
<SortOptions sortBy={sortBy} onSortChange={setSortBy} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{hasGames && (
|
|
||||||
<div>
|
|
||||||
<div className="profile-content__section-header">
|
|
||||||
<div className="profile-content__section-title-group">
|
|
||||||
<h2>{t("library")}</h2>
|
|
||||||
{userStats && (
|
|
||||||
<span className="profile-content__section-badge">
|
|
||||||
{numberFormatter.format(userStats.libraryCount)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul className="profile-content__games-grid">
|
|
||||||
{libraryGames?.map((game) => (
|
|
||||||
<li key={game.objectId} style={{ listStyle: "none" }}>
|
|
||||||
<UserLibraryGameCard
|
|
||||||
game={game}
|
|
||||||
statIndex={statsIndex}
|
|
||||||
onMouseEnter={handleOnMouseEnterGameCard}
|
|
||||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
|
||||||
sortBy={sortBy}
|
|
||||||
/>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<ProfileTabs
|
|
||||||
activeTab={activeTab}
|
|
||||||
reviewsTotalCount={reviewsTotalCount}
|
|
||||||
onTabChange={setActiveTab}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="profile-content__tab-panels">
|
|
||||||
<AnimatePresence mode="wait">
|
|
||||||
{activeTab === "library" && (
|
|
||||||
<LibraryTab
|
|
||||||
sortBy={sortBy}
|
|
||||||
onSortChange={setSortBy}
|
|
||||||
pinnedGames={pinnedGames}
|
|
||||||
libraryGames={libraryGames}
|
|
||||||
hasMoreLibraryGames={hasMoreLibraryGames}
|
|
||||||
isLoadingLibraryGames={isLoadingLibraryGames}
|
|
||||||
statsIndex={statsIndex}
|
|
||||||
userStats={userStats}
|
|
||||||
animatedGameIdsRef={animatedGameIdsRef}
|
|
||||||
onLoadMore={handleLoadMore}
|
|
||||||
onMouseEnter={handleOnMouseEnterGameCard}
|
|
||||||
onMouseLeave={handleOnMouseLeaveGameCard}
|
|
||||||
isMe={isMe}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeTab === "reviews" && (
|
|
||||||
<ReviewsTab
|
|
||||||
reviews={reviews}
|
|
||||||
isLoadingReviews={isLoadingReviews}
|
|
||||||
votingReviews={votingReviews}
|
|
||||||
userDetailsId={userDetails?.id}
|
|
||||||
formatPlayTime={formatPlayTime}
|
|
||||||
getRatingText={getRatingText}
|
|
||||||
onVote={handleVoteReview}
|
|
||||||
onDelete={handleDeleteClick}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{shouldShowRightContent && (
|
{shouldShowRightContent && (
|
||||||
@@ -669,15 +494,25 @@ export function ProfileContent() {
|
|||||||
statsIndex,
|
statsIndex,
|
||||||
libraryGames,
|
libraryGames,
|
||||||
pinnedGames,
|
pinnedGames,
|
||||||
|
|
||||||
sortBy,
|
sortBy,
|
||||||
activeTab,
|
activeTab,
|
||||||
// ensure reviews UI updates correctly
|
|
||||||
reviews,
|
reviews,
|
||||||
reviewsTotalCount,
|
reviewsTotalCount,
|
||||||
isLoadingReviews,
|
isLoadingReviews,
|
||||||
votingReviews,
|
votingReviews,
|
||||||
deleteModalVisible,
|
deleteModalVisible,
|
||||||
|
handleOnMouseEnterGameCard,
|
||||||
|
handleOnMouseLeaveGameCard,
|
||||||
|
handleImageClick,
|
||||||
|
handleLoadMore,
|
||||||
|
formatPlayTime,
|
||||||
|
getRatingText,
|
||||||
|
handleVoteReview,
|
||||||
|
handleDeleteClick,
|
||||||
|
userDetails,
|
||||||
|
animatedGameIdsRef,
|
||||||
|
hasMoreLibraryGames,
|
||||||
|
isLoadingLibraryGames,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -3,14 +3,16 @@ import { useTranslation } from "react-i18next";
|
|||||||
import "./profile-content.scss";
|
import "./profile-content.scss";
|
||||||
|
|
||||||
interface ProfileTabsProps {
|
interface ProfileTabsProps {
|
||||||
activeTab: "library" | "reviews";
|
activeTab: "library" | "reviews" | "souvenirs";
|
||||||
reviewsTotalCount: number;
|
reviewsTotalCount: number;
|
||||||
onTabChange: (tab: "library" | "reviews") => void;
|
souvenirsCount: number;
|
||||||
|
onTabChange: (tab: "library" | "reviews" | "souvenirs") => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ProfileTabs({
|
export function ProfileTabs({
|
||||||
activeTab,
|
activeTab,
|
||||||
reviewsTotalCount,
|
reviewsTotalCount,
|
||||||
|
souvenirsCount,
|
||||||
onTabChange,
|
onTabChange,
|
||||||
}: Readonly<ProfileTabsProps>) {
|
}: Readonly<ProfileTabsProps>) {
|
||||||
const { t } = useTranslation("user_profile");
|
const { t } = useTranslation("user_profile");
|
||||||
@@ -62,6 +64,29 @@ export function ProfileTabs({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="profile-content__tab-wrapper">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`profile-content__tab ${activeTab === "souvenirs" ? "profile-content__tab--active" : ""}`}
|
||||||
|
onClick={() => onTabChange("souvenirs")}
|
||||||
|
>
|
||||||
|
{t("souvenirs")}
|
||||||
|
{souvenirsCount > 0 && (
|
||||||
|
<span className="profile-content__tab-badge">{souvenirsCount}</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
{activeTab === "souvenirs" && (
|
||||||
|
<motion.div
|
||||||
|
className="profile-content__tab-underline"
|
||||||
|
layoutId="tab-underline"
|
||||||
|
transition={{
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 30,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
115
src/renderer/src/pages/profile/profile-content/souvenirs-tab.tsx
Normal file
115
src/renderer/src/pages/profile/profile-content/souvenirs-tab.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { SearchIcon } from "@primer/octicons-react";
|
||||||
|
import "./profile-content.scss";
|
||||||
|
|
||||||
|
interface Achievement {
|
||||||
|
name: string;
|
||||||
|
imageUrl: string;
|
||||||
|
achievementIcon: string | null;
|
||||||
|
gameTitle: string;
|
||||||
|
gameIconUrl: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SouvenirsTabProps {
|
||||||
|
achievements: Achievement[];
|
||||||
|
onImageClick: (imageUrl: string, achievementName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SouvenirsTab({
|
||||||
|
achievements,
|
||||||
|
onImageClick,
|
||||||
|
}: Readonly<SouvenirsTabProps>) {
|
||||||
|
const { t } = useTranslation("user_profile");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key="souvenirs"
|
||||||
|
className="profile-content__tab-panel"
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
exit={{ opacity: 0, x: 10 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
aria-hidden={false}
|
||||||
|
>
|
||||||
|
{achievements.length === 0 && (
|
||||||
|
<div className="profile-content__no-souvenirs">
|
||||||
|
<p>{t("no_souvenirs", "No souvenirs yet")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{achievements.length > 0 && (
|
||||||
|
<div className="profile-content__images-grid">
|
||||||
|
{achievements.map((achievement, index) => (
|
||||||
|
<div
|
||||||
|
key={`${achievement.gameTitle}-${achievement.name}-${index}`}
|
||||||
|
className="profile-content__image-card"
|
||||||
|
>
|
||||||
|
<div className="profile-content__image-card-header">
|
||||||
|
<div className="profile-content__image-achievement-image-wrapper">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="profile-content__image-button"
|
||||||
|
onClick={() =>
|
||||||
|
onImageClick(achievement.imageUrl, achievement.name)
|
||||||
|
}
|
||||||
|
aria-label={`View ${achievement.name} screenshot in fullscreen`}
|
||||||
|
style={{
|
||||||
|
cursor: "pointer",
|
||||||
|
padding: 0,
|
||||||
|
border: "none",
|
||||||
|
background: "transparent",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={achievement.imageUrl}
|
||||||
|
alt={achievement.name}
|
||||||
|
className="profile-content__image-achievement-image"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div className="profile-content__image-achievement-image-overlay">
|
||||||
|
<SearchIcon size={20} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="profile-content__image-card-content">
|
||||||
|
<div className="profile-content__image-achievement-info">
|
||||||
|
{achievement.achievementIcon && (
|
||||||
|
<img
|
||||||
|
src={achievement.achievementIcon}
|
||||||
|
alt=""
|
||||||
|
className="profile-content__image-achievement-icon"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="profile-content__image-achievement-name">
|
||||||
|
{achievement.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="profile-content__image-game-info">
|
||||||
|
<div className="profile-content__image-game-left">
|
||||||
|
{achievement.gameIconUrl && (
|
||||||
|
<img
|
||||||
|
src={achievement.gameIconUrl}
|
||||||
|
alt=""
|
||||||
|
className="profile-content__image-game-icon"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span className="profile-content__image-game-title">
|
||||||
|
{achievement.gameTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="profile-content__image-card-gradient-overlay"></div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -194,8 +194,8 @@ export interface ProfileAchievement {
|
|||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
unlockTime: number;
|
unlockTime: number;
|
||||||
gameTitle: string;
|
gameTitle: string;
|
||||||
gameIconUrl: string;
|
gameIconUrl: string | null;
|
||||||
achievementIcon: string;
|
achievementIcon: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserProfile {
|
export interface UserProfile {
|
||||||
|
|||||||
Reference in New Issue
Block a user