feat: add Wrapped 2025 view in profile

This commit is contained in:
Moyasee
2025-12-12 13:53:12 +02:00
parent 7f28fc8ca1
commit 0268829946
6 changed files with 214 additions and 9 deletions

View File

@@ -715,7 +715,11 @@
"karma_description": "Earned from positive likes on reviews",
"user_reviews": "Reviews",
"delete_review": "Delete Review",
"loading_reviews": "Loading reviews..."
"loading_reviews": "Loading reviews...",
"wrapped_2025": "Wrapped 2025",
"view_wrapped_title": "View {{displayName}}'s Wrapped 2025?",
"view_wrapped_yes": "Yes",
"view_wrapped_no": "No"
},
"library": {
"library": "Library",

View File

@@ -36,9 +36,9 @@ export class WindowManager {
private static initialConfigInitializationMainWindow: Electron.BrowserWindowConstructorOptions =
{
width: 1200,
height: 720,
height: 860,
minWidth: 1024,
minHeight: 540,
minHeight: 860,
backgroundColor: "#1c1c1c",
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
icon,
@@ -106,7 +106,7 @@ export class WindowManager {
valueEncoding: "json",
}
);
return data ?? { isMaximized: false, height: 720, width: 1200 };
return data ?? { isMaximized: false, height: 860, width: 1200 };
}
private static updateInitialConfig(
@@ -224,7 +224,7 @@ export class WindowManager {
? {
x: undefined,
y: undefined,
height: this.initialConfigInitializationMainWindow.height ?? 720,
height: this.initialConfigInitializationMainWindow.height ?? 860,
width: this.initialConfigInitializationMainWindow.width ?? 1200,
isMaximized: true,
}

View File

@@ -21,9 +21,10 @@ import { UserKarmaBox } from "./user-karma-box";
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 { ProfileTabs } from "./profile-tabs";
import { ProfileTabs, type ProfileTabType } from "./profile-tabs";
import { LibraryTab } from "./library-tab";
import { ReviewsTab } from "./reviews-tab";
import { WrappedConfirmModal } from "./wrapped-tab";
import { AnimatePresence } from "framer-motion";
import "./profile-content.scss";
@@ -95,7 +96,7 @@ export function ProfileContent() {
const [sortBy, setSortBy] = useState<SortOption>("playedRecently");
const statsAnimation = useRef(-1);
const [activeTab, setActiveTab] = useState<"library" | "reviews">("library");
const [activeTab, setActiveTab] = useState<ProfileTabType>("library");
// User reviews state
const [reviews, setReviews] = useState<UserReview[]>([]);
@@ -104,6 +105,7 @@ export function ProfileContent() {
const [votingReviews, setVotingReviews] = useState<Set<string>>(new Set());
const [deleteModalVisible, setDeleteModalVisible] = useState(false);
const [reviewToDelete, setReviewToDelete] = useState<string | null>(null);
const [wrappedModalVisible, setWrappedModalVisible] = useState(false);
const dispatch = useAppDispatch();
@@ -386,6 +388,7 @@ export function ProfileContent() {
activeTab={activeTab}
reviewsTotalCount={reviewsTotalCount}
onTabChange={setActiveTab}
onWrappedClick={() => setWrappedModalVisible(true)}
/>
<div className="profile-content__tab-panels">
@@ -439,6 +442,13 @@ export function ProfileContent() {
onClose={handleDeleteCancel}
onConfirm={handleDeleteConfirm}
/>
<WrappedConfirmModal
userId={userProfile.id}
displayName={userProfile.displayName}
isOpen={wrappedModalVisible}
onClose={() => setWrappedModalVisible(false)}
/>
</section>
);
}, [
@@ -460,6 +470,7 @@ export function ProfileContent() {
isLoadingReviews,
votingReviews,
deleteModalVisible,
wrappedModalVisible,
]);
return (

View File

@@ -2,16 +2,20 @@ import { motion } from "framer-motion";
import { useTranslation } from "react-i18next";
import "./profile-content.scss";
export type ProfileTabType = "library" | "reviews";
interface ProfileTabsProps {
activeTab: "library" | "reviews";
activeTab: ProfileTabType;
reviewsTotalCount: number;
onTabChange: (tab: "library" | "reviews") => void;
onTabChange: (tab: ProfileTabType) => void;
onWrappedClick: () => void;
}
export function ProfileTabs({
activeTab,
reviewsTotalCount,
onTabChange,
onWrappedClick,
}: Readonly<ProfileTabsProps>) {
const { t } = useTranslation("user_profile");
@@ -62,6 +66,15 @@ export function ProfileTabs({
/>
)}
</div>
<div className="profile-content__tab-wrapper">
<button
type="button"
className="profile-content__tab"
onClick={onWrappedClick}
>
{t("wrapped_2025")}
</button>
</div>
</div>
);
}

View File

@@ -0,0 +1,73 @@
@use "../../../scss/globals.scss";
.wrapped-fullscreen-modal {
position: fixed;
inset: 0;
z-index: 999;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
border: none;
background: transparent;
width: 100%;
height: 100%;
&__backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.9);
border: none;
z-index: 1;
}
&__container {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
padding: calc(globals.$spacing-unit * 2);
pointer-events: none;
z-index: 2;
}
&__close-button {
position: absolute;
top: calc(globals.$spacing-unit * 5);
right: calc(globals.$spacing-unit * 5);
background: rgba(255, 255, 255, 0.1);
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: white;
transition: background 0.2s ease;
z-index: 10;
pointer-events: auto;
&:hover {
background: rgba(255, 255, 255, 0.2);
}
}
&__content {
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 48px rgba(0, 0, 0, 0.5);
pointer-events: auto;
}
&__iframe {
width: 100%;
height: 100%;
border: none;
}
}

View File

@@ -0,0 +1,104 @@
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { XIcon } from "@primer/octicons-react";
import { ConfirmationModal } from "@renderer/components";
import "./wrapped-tab.scss";
interface WrappedModalProps {
userId: string;
displayName: string;
isOpen: boolean;
onClose: () => void;
}
interface ScaleConfig {
scale: number;
width: number;
height: number;
}
const SCALE_CONFIGS: Record<number, ScaleConfig> = {
0.25: { scale: 0.25, width: 270, height: 480 },
0.3: { scale: 0.3, width: 324, height: 576 },
0.5: { scale: 0.5, width: 540, height: 960 },
};
const getScaleConfigForHeight = (height: number): ScaleConfig => {
if (height >= 1000) return SCALE_CONFIGS[0.5];
if (height >= 650) return SCALE_CONFIGS[0.3];
return SCALE_CONFIGS[0.25];
};
export function WrappedConfirmModal({
userId,
displayName,
isOpen,
onClose,
}: Readonly<WrappedModalProps>) {
const { t } = useTranslation("user_profile");
const [showFullscreen, setShowFullscreen] = useState(false);
const [config, setConfig] = useState<ScaleConfig>(SCALE_CONFIGS[0.5]);
useEffect(() => {
if (!showFullscreen) return;
const updateConfig = () => {
setConfig(getScaleConfigForHeight(window.innerHeight));
};
updateConfig();
window.addEventListener("resize", updateConfig);
return () => window.removeEventListener("resize", updateConfig);
}, [showFullscreen]);
const handleConfirm = () => {
onClose();
setShowFullscreen(true);
};
return (
<>
<ConfirmationModal
visible={isOpen}
title={t("wrapped_2025")}
descriptionText={t("view_wrapped_title", { displayName })}
confirmButtonLabel={t("view_wrapped_yes")}
cancelButtonLabel={t("view_wrapped_no")}
onConfirm={handleConfirm}
onClose={onClose}
/>
{showFullscreen && (
<dialog className="wrapped-fullscreen-modal" aria-modal="true" open>
<button
type="button"
className="wrapped-fullscreen-modal__backdrop"
onClick={() => setShowFullscreen(false)}
aria-label="Close wrapped"
/>
<div className="wrapped-fullscreen-modal__container">
<button
type="button"
className="wrapped-fullscreen-modal__close-button"
onClick={() => setShowFullscreen(false)}
aria-label="Close wrapped"
>
<XIcon size={24} />
</button>
<div
className="wrapped-fullscreen-modal__content"
style={{ width: config.width, height: config.height }}
>
<iframe
src={`https://hydrawrapped.com/embed/${userId}?scale=${config.scale}`}
className="wrapped-fullscreen-modal__iframe"
title="Wrapped 2025"
/>
</div>
</div>
</dialog>
)}
</>
);
}