feat: cloud modal

This commit is contained in:
Zamitto
2024-12-22 17:38:55 -03:00
parent 041b7d520c
commit 313ebc6055
32 changed files with 304 additions and 88 deletions

View File

@@ -29,6 +29,8 @@ import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
import { downloadSourcesWorker } from "./workers";
import { repacksContext } from "./context";
import { logger } from "./logger";
import { useSubscription } from "./hooks/use-subscription";
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
export interface AppProps {
children: React.ReactNode;
@@ -47,21 +49,21 @@ export function App() {
const { indexRepacks } = useContext(repacksContext);
const {
userDetails,
hasActiveSubscription,
isFriendsModalVisible,
friendRequetsModalTab,
friendModalUserId,
syncFriendRequests,
hideFriendsModal,
} = useUserDetails();
const {
userDetails,
hasActiveSubscription,
fetchUserDetails,
updateUserDetails,
clearUserDetails,
} = useUserDetails();
const { hideHydraCloudModal, showHydraCloudModal, isHydraCloudModalVisible } =
useSubscription();
const dispatch = useAppDispatch();
const navigate = useNavigate();
@@ -308,6 +310,11 @@ export function App() {
onClose={handleToastClose}
/>
<HydraCloudModal
visible={isHydraCloudModalVisible}
onClose={hideHydraCloudModal}
/>
{userDetails && (
<UserFriendModal
visible={isFriendsModalVisible}

View File

@@ -53,7 +53,6 @@ export const gameDetailsContext = createContext<GameDetailsContext>({
setShowGameOptionsModal: () => {},
setShowRepacksModal: () => {},
setHasNSFWContentBlocked: () => {},
handleClickOpenCheckout: () => {},
});
const { Provider } = gameDetailsContext;
@@ -111,11 +110,6 @@ export function GameDetailsContextProvider({
(state) => state.userPreferences.value
);
const handleClickOpenCheckout = () => {
// TODO: show modal before redirecting to checkout page
window.electron.openCheckout();
};
const updateGame = useCallback(async () => {
return window.electron
.getGameByObjectId(objectId!)
@@ -290,7 +284,6 @@ export function GameDetailsContextProvider({
updateGame,
setShowRepacksModal,
setShowGameOptionsModal,
handleClickOpenCheckout,
}}
>
{children}

View File

@@ -29,5 +29,4 @@ export interface GameDetailsContext {
setShowRepacksModal: React.Dispatch<React.SetStateAction<boolean>>;
setShowGameOptionsModal: React.Dispatch<React.SetStateAction<boolean>>;
setHasNSFWContentBlocked: React.Dispatch<React.SetStateAction<boolean>>;
handleClickOpenCheckout: () => void;
}

View File

@@ -6,3 +6,4 @@ export * from "./window-slice";
export * from "./toast-slice";
export * from "./user-details-slice";
export * from "./running-game-slice";
export * from "./subscription-slice";

View File

@@ -0,0 +1,25 @@
import { createSlice } from "@reduxjs/toolkit";
export interface SubscriptionState {
isHydraCloudModalVisible: boolean;
}
const initialState: SubscriptionState = {
isHydraCloudModalVisible: false,
};
export const subscriptionSlice = createSlice({
name: "subscription",
initialState,
reducers: {
setHydraCloudModalVisible: (state) => {
state.isHydraCloudModalVisible = true;
},
setHydraCloudModalHidden: (state) => {
state.isHydraCloudModalVisible = false;
},
},
});
export const { setHydraCloudModalVisible, setHydraCloudModalHidden } =
subscriptionSlice.actions;

View File

@@ -0,0 +1,28 @@
import { useCallback } from "react";
import { useAppDispatch, useAppSelector } from "./redux";
import {
setHydraCloudModalVisible,
setHydraCloudModalHidden,
} from "@renderer/features";
export function useSubscription() {
const dispatch = useAppDispatch();
const { isHydraCloudModalVisible } = useAppSelector(
(state) => state.subscription
);
const showHydraCloudModal = useCallback(() => {
dispatch(setHydraCloudModalVisible());
}, [dispatch]);
const hideHydraCloudModal = useCallback(() => {
dispatch(setHydraCloudModalHidden());
}, [dispatch]);
return {
isHydraCloudModalVisible,
showHydraCloudModal,
hideHydraCloudModal,
};
}

View File

@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
import * as styles from "./achievements.css";
import { EyeClosedIcon } from "@primer/octicons-react";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { useSubscription } from "@renderer/hooks/use-subscription";
import { vars } from "@renderer/theme.css";
interface AchievementListProps {
achievements: UserAchievement[];
@@ -11,6 +13,7 @@ interface AchievementListProps {
export function AchievementList({ achievements }: AchievementListProps) {
const { t } = useTranslation("achievement");
const { showHydraCloudModal } = useSubscription();
const { formatDateTime } = useDate();
return (
@@ -41,7 +44,7 @@ export function AchievementList({ achievements }: AchievementListProps) {
<p>{achievement.description}</p>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
{achievement.points && (
{achievement.points != undefined ? (
<div
style={{ display: "flex", alignItems: "center", gap: "4px" }}
title={t("achievement_earn_points", {
@@ -51,6 +54,23 @@ export function AchievementList({ achievements }: AchievementListProps) {
<HydraIcon width={20} height={20} />
<p style={{ fontSize: "1.1em" }}>{achievement.points}</p>
</div>
) : (
<button
onClick={showHydraCloudModal}
style={{
display: "flex",
alignItems: "center",
gap: "4px",
cursor: "pointer",
color: vars.color.warning,
}}
title={t("achievement_earn_points", {
points: "???",
})}
>
<HydraIcon width={20} height={20} />
<p style={{ fontSize: "1.1em" }}>???</p>
</button>
)}
{achievement.unlockTime && (
<div

View File

@@ -19,6 +19,7 @@ import * as styles from "./achievements.css";
import { AchievementList } from "./achievement-list";
import { AchievementPanel } from "./achievement-panel";
import { ComparedAchievementPanel } from "./compared-achievement-panel";
import { useSubscription } from "@renderer/hooks/use-subscription";
interface UserInfo {
id: string;
@@ -41,7 +42,7 @@ interface AchievementSummaryProps {
function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
const { t } = useTranslation("achievement");
const { userDetails, hasActiveSubscription } = useUserDetails();
const { handleClickOpenCheckout } = useContext(gameDetailsContext);
const { showHydraCloudModal } = useSubscription();
const getProfileImage = (
user: Pick<UserInfo, "profileImageUrl" | "displayName">
@@ -92,7 +93,7 @@ function AchievementSummary({ user, isComparison }: AchievementSummaryProps) {
<h3>
<button
className={styles.subscriptionRequiredButton}
onClick={handleClickOpenCheckout}
onClick={showHydraCloudModal}
>
{t("subscription_needed")}
</button>

View File

@@ -14,6 +14,7 @@ import { steamUrlBuilder } from "@shared";
import cloudIconAnimated from "@renderer/assets/icons/cloud-animated.gif";
import { useUserDetails } from "@renderer/hooks";
import { useSubscription } from "@renderer/hooks/use-subscription";
const HERO_ANIMATION_THRESHOLD = 25;
@@ -31,9 +32,10 @@ export function GameDetailsContent() {
gameColor,
setGameColor,
hasNSFWContentBlocked,
handleClickOpenCheckout,
} = useContext(gameDetailsContext);
const { showHydraCloudModal } = useSubscription();
const { userDetails, hasActiveSubscription } = useUserDetails();
const { setShowCloudSyncModal, getGameArtifacts } =
@@ -104,7 +106,7 @@ export function GameDetailsContent() {
}
if (!hasActiveSubscription) {
handleClickOpenCheckout();
showHydraCloudModal();
return;
}

View File

@@ -21,6 +21,8 @@ import { howLongToBeatEntriesTable } from "@renderer/dexie";
import { SidebarSection } from "../sidebar-section/sidebar-section";
import { buildGameAchievementPath } from "@renderer/helpers";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useSubmit } from "react-router-dom";
import { useSubscription } from "@renderer/hooks/use-subscription";
const fakeAchievements: UserAchievement[] = [
{
@@ -67,15 +69,10 @@ export function Sidebar() {
const [activeRequirement, setActiveRequirement] =
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
const {
gameTitle,
shopDetails,
objectId,
shop,
stats,
achievements,
handleClickOpenCheckout,
} = useContext(gameDetailsContext);
const { gameTitle, shopDetails, objectId, shop, stats, achievements } =
useContext(gameDetailsContext);
const { showHydraCloudModal } = useSubscription();
const { t } = useTranslation("game_details");
const { formatDateTime } = useDate();
@@ -179,7 +176,7 @@ export function Sidebar() {
{!hasActiveSubscription && (
<button
className={styles.subscriptionRequiredButton}
onClick={handleClickOpenCheckout}
onClick={showHydraCloudModal}
>
<CloudOfflineIcon size={16} />
<span>{t("achievements_not_sync")}</span>

View File

@@ -203,3 +203,12 @@ export const achievementsProgressBar = style({
borderRadius: "4px",
},
});
export const link = style({
textAlign: "start",
color: vars.color.body,
":hover": {
textDecoration: "underline",
cursor: "pointer",
},
});

View File

@@ -5,8 +5,11 @@ import { useTranslation } from "react-i18next";
import { useFormat } from "@renderer/hooks";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import HydraIcon from "@renderer/assets/icons/hydra.svg?react";
import { useSubscription } from "@renderer/hooks/use-subscription";
export function UserStatsBox() {
const { showHydraCloudModal } = useSubscription();
const { userStats } = useContext(userProfileContext);
const { t } = useTranslation("user_profile");
@@ -40,37 +43,49 @@ export function UserStatsBox() {
<div className={styles.box}>
<ul className={styles.list}>
{userStats.achievementsPointsEarnedSum && (
<li>
<h3 className={styles.listItemTitle}>{t("achievements")}</h3>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<p
style={{ display: "flex", alignItems: "center", gap: "4px" }}
<li>
<h3 className={styles.listItemTitle}>{t("achievements")}</h3>
{userStats.achievementsPointsEarnedSum !== undefined ? (
<>
<div
style={{ display: "flex", justifyContent: "space-between" }}
>
<HydraIcon width={20} height={20} />
{userStats.achievementsPointsEarnedSum.value}
</p>
<p title={t("ranking_updated_weekly")}>
{t("top_percentile", {
percentile:
userStats.achievementsPointsEarnedSum.topPercentile,
})}
</p>
</div>
<p>Unlock count: {userStats.unlockedAchievementSum}</p>
</li>
)}
<p
style={{
display: "flex",
alignItems: "center",
gap: "4px",
}}
>
<HydraIcon width={20} height={20} />
{userStats.achievementsPointsEarnedSum.value}
</p>
<p title={t("ranking_updated_weekly")}>
{t("top_percentile", {
percentile:
userStats.achievementsPointsEarnedSum.topPercentile,
})}
</p>
</div>
<p>Unlock count: {userStats.unlockedAchievementSum}</p>
</>
) : (
<button
type="button"
onClick={showHydraCloudModal}
className={styles.link}
>
<small>
Saiba como exibir suas conquistas e pontos no perfil
</small>
</button>
)}
</li>
<li>
<h3 className={styles.listItemTitle}>{t("games")}</h3>
<h3 className={styles.listItemTitle}>{t("total_play_time")}</h3>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<p>
{t("total_play_time", {
amount: formatPlayTime(
userStats.totalPlayTimeInSeconds.value
),
})}
</p>
<p>{formatPlayTime(userStats.totalPlayTimeInSeconds.value)}</p>
<p title={t("ranking_updated_weekly")}>
{t("top_percentile", {
percentile: userStats.totalPlayTimeInSeconds.topPercentile,

View File

@@ -0,0 +1,79 @@
import { SPACING_UNIT, vars } from "../../../theme.css";
import { style } from "@vanilla-extract/css";
export const friendListDisplayName = style({
fontWeight: "bold",
fontSize: vars.size.body,
textAlign: "left",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
});
export const friendListContainer = style({
display: "flex",
gap: `${SPACING_UNIT * 3}px`,
alignItems: "center",
borderRadius: "4px",
border: `solid 1px ${vars.color.border}`,
width: "100%",
height: "54px",
minHeight: "54px",
transition: "all ease 0.2s",
position: "relative",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const friendListButton = style({
display: "flex",
alignItems: "center",
position: "absolute",
cursor: "pointer",
height: "100%",
width: "100%",
flexDirection: "row",
color: vars.color.body,
gap: `${SPACING_UNIT + SPACING_UNIT / 2}px`,
padding: `0 ${SPACING_UNIT}px`,
});
export const friendRequestItem = style({
color: vars.color.body,
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const acceptRequestButton = style({
cursor: "pointer",
color: vars.color.body,
width: "28px",
height: "28px",
":hover": {
color: vars.color.success,
},
});
export const cancelRequestButton = style({
cursor: "pointer",
color: vars.color.body,
width: "28px",
height: "28px",
":hover": {
color: vars.color.danger,
},
});
export const friendCodeButton = style({
color: vars.color.body,
cursor: "pointer",
display: "flex",
gap: `${SPACING_UNIT / 2}px`,
alignItems: "center",
transition: "all ease 0.2s",
":hover": {
color: vars.color.muted,
},
});

View File

@@ -0,0 +1,36 @@
import { Button, Modal } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import { useTranslation } from "react-i18next";
export interface HydraCloudModalProps {
visible: boolean;
onClose: () => void;
}
export const HydraCloudModal = ({ visible, onClose }: HydraCloudModalProps) => {
const { t } = useTranslation("hydra_cloud");
const handleClickOpenCheckout = () => {
window.electron.openCheckout();
};
return (
<Modal
visible={visible}
title={t("subscription_tour_title")}
onClose={onClose}
>
<div
style={{
display: "flex",
width: "500px",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
}}
>
Você descobriu uma funcionalidade Hydra Cloud!
<Button onClick={handleClickOpenCheckout}>Saiba mais</Button>
</div>
</Modal>
);
};

View File

@@ -8,6 +8,7 @@ import {
toastSlice,
userDetailsSlice,
gameRunningSlice,
subscriptionSlice,
} from "@renderer/features";
export const store = configureStore({
@@ -20,6 +21,7 @@ export const store = configureStore({
toast: toastSlice.reducer,
userDetails: userDetailsSlice.reducer,
gameRunning: gameRunningSlice.reducer,
subscription: subscriptionSlice.reducer,
},
});