mirror of
https://github.com/hydralauncher/hydra.git
synced 2026-01-18 16:53:57 +00:00
feat: add Wrapped 2025 view in profile
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
104
src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx
Normal file
104
src/renderer/src/pages/profile/profile-content/wrapped-tab.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user