clear last tickets
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { Platform, Pressable, StyleSheet } from "react-native";
|
||||
import CreditAmount from "./CreditAmount";
|
||||
import CoinPackModal from "./modal/CoinPackModal";
|
||||
import { useUser } from "../providers/UserDataProvider";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import { openCoinPackModal } from "../utils/coinPackModal";
|
||||
|
||||
const MobileCoinBadge = ({
|
||||
style,
|
||||
textStyle,
|
||||
iconSize = 20,
|
||||
iconPosition = "left",
|
||||
}) => {
|
||||
const { currentUID, currentUserData } = useUser() || {};
|
||||
const [isModalVisible, setModalVisible] = useState(false);
|
||||
|
||||
const coinBalance = 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 handlePress = useCallback(() => {
|
||||
if (Platform.OS === "web") {
|
||||
openCoinPackModal();
|
||||
return;
|
||||
}
|
||||
setModalVisible(true);
|
||||
}, []);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setModalVisible(false);
|
||||
}, []);
|
||||
|
||||
if (!currentUID || Platform.OS === "web") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Pressable
|
||||
onPress={handlePress}
|
||||
accessibilityRole="button"
|
||||
style={[styles.container, style]}
|
||||
>
|
||||
<CreditAmount
|
||||
value={coinBalance}
|
||||
style={styles.content}
|
||||
textStyle={[styles.text, textStyle]}
|
||||
iconSize={iconSize}
|
||||
iconPosition={iconPosition}
|
||||
/>
|
||||
</Pressable>
|
||||
<CoinPackModal visible={isModalVisible} onClose={handleClose} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "rgba(12, 14, 18, 0.72)",
|
||||
borderWidth: 1,
|
||||
borderColor: "rgba(255, 255, 255, 0.1)",
|
||||
flexShrink: 1,
|
||||
},
|
||||
content: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
},
|
||||
text: {
|
||||
fontSize: 18,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
});
|
||||
|
||||
export default MobileCoinBadge;
|
||||
@@ -45,6 +45,7 @@ const formatCurrency = (amount, currency = "eur") => {
|
||||
|
||||
const FALLBACK_BASE_PRICE_PER_COIN = 12; // ~0,12€ par jeton (valeur indicatif pour l'affichage)
|
||||
const FALLBACK_DISCOUNT_STEPS = [0, 12, 18, 26, 32];
|
||||
const STATIC_DISCOUNTS = [0, 30, 50];
|
||||
|
||||
const computeCoinPackPricing = (packs = []) => {
|
||||
if (!Array.isArray(packs) || !packs.length) {
|
||||
@@ -61,9 +62,7 @@ const computeCoinPackPricing = (packs = []) => {
|
||||
return {
|
||||
...pack,
|
||||
hasValidPrice,
|
||||
pricePerCoin: hasValidPrice
|
||||
? pack.unitAmount / pack.coinAmount
|
||||
: null,
|
||||
pricePerCoin: hasValidPrice ? pack.unitAmount / pack.coinAmount : null,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -146,8 +145,7 @@ const computeCoinPackPricing = (packs = []) => {
|
||||
discountPercent,
|
||||
isBasePack,
|
||||
isBestValue:
|
||||
(bestDiscountPack &&
|
||||
bestDiscountPack.productId === pack.productId) ||
|
||||
(bestDiscountPack && bestDiscountPack.productId === pack.productId) ||
|
||||
(!bestDiscountPack && fallbackBestId === pack.productId),
|
||||
};
|
||||
|
||||
@@ -155,7 +153,13 @@ const computeCoinPackPricing = (packs = []) => {
|
||||
}, {});
|
||||
};
|
||||
|
||||
function CoinPackCard({ pack, selected, pricingDetail, onSelect }) {
|
||||
function CoinPackCard({
|
||||
pack,
|
||||
selected,
|
||||
pricingDetail,
|
||||
onSelect,
|
||||
staticDiscountPercent,
|
||||
}) {
|
||||
const handleSelect = React.useCallback(() => {
|
||||
if (typeof onSelect !== "function" || !pack?.productId) {
|
||||
return;
|
||||
@@ -166,11 +170,20 @@ function CoinPackCard({ pack, selected, pricingDetail, onSelect }) {
|
||||
const formattedPrice = formatCurrency(pack?.unitAmount, pack?.currency);
|
||||
const perCoinPrice = pricingDetail?.formattedPricePerCoin;
|
||||
const discountPercent = pricingDetail?.discountPercent;
|
||||
const discountLabel = pricingDetail?.isBasePack
|
||||
? "Pack de base (référence)"
|
||||
: typeof discountPercent === "number"
|
||||
? `-${discountPercent}% vs pack de base`
|
||||
: null;
|
||||
const hasStaticDiscount = typeof staticDiscountPercent === "number";
|
||||
const effectiveDiscountPercent = hasStaticDiscount
|
||||
? staticDiscountPercent
|
||||
: discountPercent;
|
||||
const isBaseReference = !hasStaticDiscount && pricingDetail?.isBasePack;
|
||||
const discountLabel = hasStaticDiscount
|
||||
? staticDiscountPercent > 0
|
||||
? `-${staticDiscountPercent}%`
|
||||
: null
|
||||
: isBaseReference
|
||||
? "Pack de base (référence)"
|
||||
: typeof discountPercent === "number"
|
||||
? `-${discountPercent}% vs pack de base`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
@@ -184,6 +197,11 @@ function CoinPackCard({ pack, selected, pricingDetail, onSelect }) {
|
||||
accessibilityState={{ selected }}
|
||||
>
|
||||
<BlurView intensity={20} tint="dark" style={styles.cardBlur}>
|
||||
{/*{pricingDetail?.isBestValue ? (*/}
|
||||
{/* <View style={styles.tagBestValue}>*/}
|
||||
{/* <Text style={styles.tagBestValueText}>Meilleure offre</Text>*/}
|
||||
{/* </View>*/}
|
||||
{/*) : null}*/}
|
||||
<View style={styles.cardHeader}>
|
||||
<CreditAmount
|
||||
value={pack?.coinAmount}
|
||||
@@ -191,55 +209,39 @@ function CoinPackCard({ pack, selected, pricingDetail, onSelect }) {
|
||||
textStyle={styles.coinAmount}
|
||||
iconSize={26}
|
||||
/>
|
||||
{pack?.name ? <Text style={styles.packName}>{pack.name}</Text> : null}
|
||||
{pricingDetail?.isBestValue ? (
|
||||
<View style={styles.tagBestValue}>
|
||||
<Text style={styles.tagBestValueText}>Meilleure offre</Text>
|
||||
{discountLabel ? (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: 0,
|
||||
top: -5,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={[
|
||||
styles.discountBadge,
|
||||
isBaseReference && styles.discountBadgeBase,
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.discountText]}>{discountLabel}</Text>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
{pack?.name ? <Text style={styles.packName}>{pack.name}</Text> : null}
|
||||
</View>
|
||||
<View style={styles.perksList}>
|
||||
<Text style={styles.perkItem}>
|
||||
- Diffusion sur mes plates formes de streaming
|
||||
</Text>
|
||||
<Text style={styles.perkItem}>
|
||||
*Eligible au Hit parade Chanson/Video
|
||||
</Text>
|
||||
</View>
|
||||
{pack?.description ? (
|
||||
<Text style={styles.packDescription}>{pack.description}</Text>
|
||||
) : null}
|
||||
<View style={styles.priceBlock}>
|
||||
{formattedPrice ? (
|
||||
<Text style={styles.packPrice}>{formattedPrice}</Text>
|
||||
) : null}
|
||||
<Text style={styles.pricePerCoin} numberOfLines={1}>
|
||||
{perCoinPrice
|
||||
? `≈ ${perCoinPrice} / jeton`
|
||||
: "Valeurs indicatives / jeton"}
|
||||
</Text>
|
||||
</View>
|
||||
{discountLabel ? (
|
||||
<View style={styles.discountRow}>
|
||||
<View
|
||||
style={[
|
||||
styles.discountBadge,
|
||||
pricingDetail?.isBestValue && styles.discountBadgeBest,
|
||||
pricingDetail?.isBasePack && styles.discountBadgeBase,
|
||||
]}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.discountText,
|
||||
pricingDetail?.isBestValue && styles.discountTextBest,
|
||||
]}
|
||||
>
|
||||
{discountLabel}
|
||||
</Text>
|
||||
</View>
|
||||
{!pricingDetail?.isBasePack && typeof discountPercent === "number" ? (
|
||||
<Text style={styles.discountHelper}>
|
||||
Économie estimée vs pack de base
|
||||
</Text>
|
||||
) : (
|
||||
<Text style={styles.discountHelperMuted}>
|
||||
Référence prix/jeton
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
);
|
||||
@@ -359,13 +361,14 @@ const CoinPackModal = ({ visible, onClose }) => {
|
||||
]}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
{coinPacks.map((pack) => (
|
||||
{coinPacks.map((pack, index) => (
|
||||
<CoinPackCard
|
||||
key={pack.productId}
|
||||
pack={pack}
|
||||
selected={selectedPackId === pack.productId}
|
||||
pricingDetail={packPricingById[pack.productId]}
|
||||
onSelect={setSelectedPackId}
|
||||
staticDiscountPercent={STATIC_DISCOUNTS[index]}
|
||||
/>
|
||||
))}
|
||||
</ScrollView>
|
||||
@@ -564,6 +567,16 @@ const styles = StyleSheet.create({
|
||||
color: "rgba(255, 255, 255, 0.72)",
|
||||
textAlign: "center",
|
||||
},
|
||||
perksList: {
|
||||
gap: 6,
|
||||
alignItems: "center",
|
||||
},
|
||||
perkItem: {
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontSize: 13,
|
||||
color: "rgba(255, 255, 255, 0.72)",
|
||||
textAlign: "center",
|
||||
},
|
||||
priceBlock: {
|
||||
gap: 2,
|
||||
alignItems: "center",
|
||||
@@ -580,10 +593,6 @@ const styles = StyleSheet.create({
|
||||
color: "rgba(255, 255, 255, 0.7)",
|
||||
textAlign: "center",
|
||||
},
|
||||
discountRow: {
|
||||
alignItems: "center",
|
||||
gap: 6,
|
||||
},
|
||||
discountBadge: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
@@ -626,6 +635,9 @@ const styles = StyleSheet.create({
|
||||
paddingVertical: 4,
|
||||
borderRadius: 10,
|
||||
backgroundColor: Palette.transparentGreen,
|
||||
alignSelf: "center",
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
},
|
||||
tagBestValueText: {
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
|
||||
import Slider from "../Slider";
|
||||
|
||||
const clamp01 = (value) => Math.min(1, Math.max(0, value || 0));
|
||||
|
||||
const formatTime = (ms) => {
|
||||
const totalSeconds = Math.max(0, Math.floor((Number(ms) || 0) / 1000));
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
.toString()
|
||||
.padStart(1, "0");
|
||||
const seconds = (totalSeconds % 60).toString().padStart(2, "0");
|
||||
return `${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const ProgressSlider = ({
|
||||
positionMs = 0,
|
||||
durationMs = 0,
|
||||
isPlaying = false,
|
||||
onSeek, // (targetMs) => void | Promise<void>
|
||||
onPause, // () => void | Promise<void>
|
||||
onPlay, // () => void | Promise<void>
|
||||
onSeekStart, // () => void | Promise<void>
|
||||
onSeekEnd, // () => void | Promise<void>
|
||||
disabled = false,
|
||||
}) => {
|
||||
const wasPlayingRef = useRef(false);
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
const [previewRatio, setPreviewRatio] = useState(null);
|
||||
|
||||
const { safePosition, safeDuration, progress } = useMemo(() => {
|
||||
const dur = Math.max(0, Number(durationMs) || 0);
|
||||
const pos = Math.max(0, Math.min(dur, Number(positionMs) || 0));
|
||||
const ratio = dur > 0 ? clamp01(pos / dur) : 0;
|
||||
return { safePosition: pos, safeDuration: dur, progress: ratio };
|
||||
}, [durationMs, positionMs]);
|
||||
|
||||
const handleSeekStart = useCallback(() => {
|
||||
if (disabled || !safeDuration) return;
|
||||
wasPlayingRef.current = !!isPlaying;
|
||||
setIsSeeking(true);
|
||||
setPreviewRatio(progress);
|
||||
if (typeof onSeekStart === "function") {
|
||||
onSeekStart();
|
||||
}
|
||||
if (isPlaying && typeof onPause === "function") {
|
||||
onPause();
|
||||
}
|
||||
}, [disabled, safeDuration, isPlaying, onPause, onSeekStart]);
|
||||
|
||||
const handleSeek = useCallback(
|
||||
(ratio) => {
|
||||
if (disabled || !safeDuration || typeof onSeek !== "function") return;
|
||||
const bounded = clamp01(ratio);
|
||||
setPreviewRatio(bounded);
|
||||
const targetMs = safeDuration * bounded;
|
||||
onSeek(targetMs);
|
||||
},
|
||||
[disabled, onSeek, safeDuration],
|
||||
);
|
||||
|
||||
const handleSeekEnd = useCallback(() => {
|
||||
if (disabled || !safeDuration) return;
|
||||
setIsSeeking(false);
|
||||
setPreviewRatio(null);
|
||||
if (typeof onSeekEnd === "function") {
|
||||
onSeekEnd();
|
||||
}
|
||||
if (wasPlayingRef.current && typeof onPlay === "function") {
|
||||
onPlay();
|
||||
}
|
||||
wasPlayingRef.current = false;
|
||||
}, [disabled, onPlay, onSeekEnd, safeDuration]);
|
||||
|
||||
return (
|
||||
<Slider
|
||||
value={formatTime(
|
||||
isSeeking && previewRatio != null
|
||||
? safeDuration * previewRatio
|
||||
: safePosition,
|
||||
)}
|
||||
maxValue={formatTime(safeDuration)}
|
||||
progress={progress}
|
||||
seekEnabled={!disabled && safeDuration > 0}
|
||||
onSeekStart={handleSeekStart}
|
||||
onSeek={handleSeek}
|
||||
onSeekEnd={handleSeekEnd}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProgressSlider;
|
||||
Reference in New Issue
Block a user