Files
musicland/src/layouts/Page.js
T
2025-12-03 16:11:44 +01:00

331 lines
9.5 KiB
JavaScript

/* eslint-disable react/display-name */
import { Image, Pressable, Text, View, StyleSheet } from "react-native";
import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view";
import { SafeAreaView } from "react-native-safe-area-context";
import React from "reactn";
import { responsiveHeight } from "../actions/responsiveSizes.js";
import { subBadges } from "../assets";
import BaseHeader from "../components/BaseHeader";
import CreditAmount from "../components/CreditAmount";
import ConnectBtn from "../components/ConnectBtn.js";
import CoinPackModal from "../components/modal/CoinPackModal";
import NavigateHeader from "../components/NavigateHeader";
import ShareBtn from "../components/ShareBtn/ShareBtn";
import { isWeb } from "../hooks/useLayoutType.js";
import { useUserData } from "../providers/UserDataProvider";
import { Routes } from "../navigation";
import { navigate } from "../navigation/NavigationService";
import { gutters } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { subscribeCoinPackModal } from "../utils/coinPackModal";
export default ({
children,
topStickyContent = null,
bottomStickyContent = null,
title = "",
rightComponent = null,
containerType = "SAFE_AREA_VIEW",
headerType = "BASE",
containerStyle = {},
headerStyle = {},
contentContainerStyle = {},
scrollEnabled = false,
onBackPressed = null,
backgroundImg = null,
headerTitleStyle = {},
hideBackButton = false,
width = undefined,
maxWidth = 1200,
shareBtn = false,
connect = false,
blurIntensity = 0,
backgroundColor = "#000",
showCoin = true,
}) => {
const { currentUID = null, currentUserData = null } = useUserData?.() || {};
const coinBalance = React.useMemo(() => {
const value = currentUserData?.coins;
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return 0;
}, [currentUserData?.coins]);
const showCoinBadge = isWeb && !!currentUID && showCoin;
const [isCoinModalVisible, setCoinModalVisible] = React.useState(false);
const subscriptionBadgeSource = React.useMemo(() => {
const allowedLevels = new Set(["starter", "pro", "premium"]);
const pickLevel = (value) => {
if (typeof value !== "string") {
return null;
}
const normalized = value.trim().toLowerCase();
return normalized && allowedLevels.has(normalized) ? normalized : null;
};
const nestedSubscription =
currentUserData?.subscription &&
typeof currentUserData.subscription === "object"
? currentUserData.subscription
: null;
const stripeMetadata =
currentUserData?.stripeSubscription &&
typeof currentUserData.stripeSubscription === "object" &&
typeof currentUserData.stripeSubscription.metadata === "object"
? currentUserData.stripeSubscription.metadata
: null;
const directCandidates = [
pickLevel(currentUserData?.premiumLevel),
pickLevel(currentUserData?.subscriptionLevel),
pickLevel(currentUserData?.subscriptionPlan),
pickLevel(currentUserData?.subscriptionPack),
];
const nestedCandidates = nestedSubscription
? ["level", "plan", "pack", "type"].map((key) =>
pickLevel(nestedSubscription?.[key]),
)
: [];
const metadataCandidates = stripeMetadata
? [
"level",
"subscriptionLevel",
"subscription_level",
"pack",
"Pack",
].map((key) => pickLevel(stripeMetadata?.[key]))
: [];
const resolvedLevel = [
...directCandidates,
...nestedCandidates,
...metadataCandidates,
].find(Boolean);
return resolvedLevel ? subBadges[resolvedLevel] || null : null;
}, [currentUserData]);
const handleCoinPress = React.useCallback(() => {
if (!showCoinBadge) return;
setCoinModalVisible(true);
}, [showCoinBadge]);
const handleCloseCoinModal = React.useCallback(() => {
setCoinModalVisible(false);
}, []);
React.useEffect(() => {
if (!showCoinBadge && isCoinModalVisible) {
setCoinModalVisible(false);
}
}, [showCoinBadge, isCoinModalVisible]);
React.useEffect(() => {
const unsubscribe = subscribeCoinPackModal(() => {
if (!showCoinBadge) {
return;
}
setCoinModalVisible(true);
});
return unsubscribe;
}, [showCoinBadge]);
const PageContainer =
containerType === "SAFE_AREA_VIEW" ? SafeAreaView : View;
const ContentContainer = scrollEnabled ? KeyboardAwareScrollView : View;
const computedWidth = width ?? (isWeb ? "60%" : "100%");
const shareButtonElement = (() => {
if (!shareBtn) return null;
if (typeof React?.isValidElement === "function") {
if (React.isValidElement(shareBtn)) {
return shareBtn;
}
}
if (typeof shareBtn === "object" && !Array.isArray(shareBtn)) {
return <ShareBtn {...shareBtn} />;
}
return <ShareBtn />;
})();
const resolvedContainerStyle = React.useMemo(
() => StyleSheet.flatten(containerStyle) || {},
[containerStyle],
);
const resolvedHeaderStyle = React.useMemo(
() => StyleSheet.flatten(headerStyle) || {},
[headerStyle],
);
const resolvedContentContainerStyle = React.useMemo(
() => StyleSheet.flatten(contentContainerStyle) || {},
[contentContainerStyle],
);
return (
<View
style={{
flex: 1,
minHeight: isWeb ? "100svh" : "100%",
position: "relative",
backgroundColor: backgroundColor,
}}
>
{backgroundImg ? (
<Image
source={backgroundImg}
resizeMode="cover"
blurIntensity={blurIntensity}
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
...(isWeb && blurIntensity
? { filter: `blur(${blurIntensity}px)` }
: {}),
}}
/>
) : null}
{showCoinBadge ? (
<View
style={{
position: "absolute",
top: gutters,
left: gutters,
flexDirection: "row",
alignItems: "center",
gap: 6,
zIndex: 20,
}}
>
<Pressable
onPress={handleCoinPress}
accessibilityRole="button"
style={{
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: "rgba(12, 14, 18, 0.72)",
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.1)",
flexDirection: "row",
alignItems: "center",
gap: 8,
}}
>
<CreditAmount
value={coinBalance}
iconSize={22}
textStyle={{
color: "#ffffff",
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 14,
}}
/>
</Pressable>
{subscriptionBadgeSource ? (
<Pressable
onPress={() => navigate(Routes.ManageSubscription)}
accessibilityRole="button"
style={{ paddingVertical: 4 }}
>
<Image
source={subscriptionBadgeSource}
style={{ height: 50, width: 50, resizeMode: "contain" }}
/>
</Pressable>
) : null}
</View>
) : null}
<PageContainer
{...(containerType === "SAFE_AREA_VIEW" ? { edges: ["top"] } : {})}
style={{
height: "100%",
paddingTop: isWeb ? gutters / 2 : 0,
padding: gutters,
paddingBottom: 0,
...(isWeb ? { maxHeight: "100svh" } : {}),
width: computedWidth,
maxWidth: maxWidth ? maxWidth : null,
alignSelf: isWeb ? "center" : "auto",
...resolvedContainerStyle,
}}
>
{headerType !== "NONE" ? (
headerType === "BASE" ? (
<BaseHeader containerStyle={{ ...resolvedHeaderStyle }} />
) : (
<NavigateHeader
containerStyle={{ ...resolvedHeaderStyle }}
title={title}
rightComponent={rightComponent}
onBackPressed={onBackPressed}
headerTitleStyle={headerTitleStyle}
hideBackButton={hideBackButton}
/>
)
) : null}
{topStickyContent?.()}
<ContentContainer
style={{ flex: 1, ...resolvedContentContainerStyle }}
{...(scrollEnabled
? {
scrollEventThrottle: 80,
showsVerticalScrollIndicator: false,
contentContainerStyle: { paddingBottom: responsiveHeight(55) },
keyboardDismissMode: "on-drag",
}
: {})}
>
{children}
</ContentContainer>
</PageContainer>
{bottomStickyContent?.()}
{(shareButtonElement || connect) && isWeb && (
<View
style={{
position: "absolute",
top: 20,
right: 20,
flexDirection: "row",
alignItems: "center",
gap: 12, // ou marginLeft sur chaque bouton si gap non supporté
}}
>
{shareButtonElement}
{connect && <ConnectBtn />}
</View>
)}
<CoinPackModal
visible={isCoinModalVisible}
onClose={handleCloseCoinModal}
/>
</View>
);
};