classement tout les mois

This commit is contained in:
2025-10-21 16:52:47 +02:00
parent 3aa5907b51
commit 60e64fa892
3 changed files with 220 additions and 60 deletions
+1
View File
@@ -28,3 +28,4 @@ exports.thumbnail = require("./src/thumbnail");
exports.upload = require("./src/upload"); exports.upload = require("./src/upload");
exports.algolia = require("./src/algolia"); exports.algolia = require("./src/algolia");
exports.notifications = require("./src/notifications"); exports.notifications = require("./src/notifications");
exports.rankings = require("./src/rankings");
+91
View File
@@ -0,0 +1,91 @@
const admin = require("firebase-admin");
const { onSchedule } = require("firebase-functions/v2/scheduler");
const { logger } = require("firebase-functions/logger");
const db = admin.firestore();
function buildMonthContext(referenceDate) {
const current = referenceDate ? new Date(referenceDate) : new Date();
current.setHours(0, 0, 0, 0);
current.setDate(1);
const target = new Date(current);
target.setMonth(target.getMonth() - 1);
const month = target.getMonth();
const year = target.getFullYear();
const monthKey = `${year}-${String(month + 1).padStart(2, "0")}`;
const rangeStart = new Date(year, month, 1, 0, 0, 0, 0);
const rangeEnd = new Date(year, month + 1, 0, 23, 59, 59, 999);
return {
year,
month: month + 1,
monthKey,
rangeStart,
rangeEnd,
};
}
exports.snapshotMonthlyTopSongs = onSchedule(
{
schedule: "5 0 1 * *",
timeZone: "Europe/Paris",
},
async (event) => {
const { scheduleTime } = event;
const context = buildMonthContext(
scheduleTime ? new Date(scheduleTime) : new Date(),
);
logger.info(
`[snapshotMonthlyTopSongs] Start ranking for ${context.monthKey}`,
);
const topProjectsSnap = await db
.collection("projects")
.orderBy("views", "desc")
.limit(3)
.get();
const topProjects = topProjectsSnap.docs.map((doc, index) => {
const data = doc.data() || {};
return {
rank: index + 1,
projectId: doc.id,
title: data.title || null,
userId: data.userId || null,
userName: data.userName || null,
coverUrl: data.coverUrl || null,
songUrl: data.songUrl || null,
views: data.views || 0,
};
});
const docRef = db.collection("monthlyTopSongs").doc(context.monthKey);
const existingSnapshot = await docRef.get();
const payload = {
monthKey: context.monthKey,
month: context.month,
year: context.year,
range: {
start: admin.firestore.Timestamp.fromDate(context.rangeStart),
end: admin.firestore.Timestamp.fromDate(context.rangeEnd),
},
topProjects,
totalProjects: topProjects.length,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
};
if (!existingSnapshot.exists) {
payload.createdAt = admin.firestore.FieldValue.serverTimestamp();
}
await docRef.set(payload, { merge: true });
logger.info(
`[snapshotMonthlyTopSongs] Stored ${topProjects.length} projects for ${context.monthKey}`,
);
},
);
+119 -51
View File
@@ -1,4 +1,5 @@
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import { Portal } from "@gorhom/portal";
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
@@ -52,8 +53,27 @@ const ProjectDropDown = ({
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [triggerLayout, setTriggerLayout] = useState(null); const [triggerLayout, setTriggerLayout] = useState(null);
const [triggerWindowLayout, setTriggerWindowLayout] = useState(null);
const triggerRef = useRef(null);
const dropdownAnimation = useRef(new Animated.Value(0)).current; const dropdownAnimation = useRef(new Animated.Value(0)).current;
const measureTriggerPosition = useCallback(() => {
if (triggerRef.current?.measureInWindow) {
try {
triggerRef.current.measureInWindow((x, y, width, height) => {
setTriggerWindowLayout({
x: x ?? 0,
y: y ?? 0,
width: width ?? triggerLayout?.width ?? 0,
height: height ?? triggerLayout?.height ?? 0,
});
});
} catch (error) {
console.warn("ProjectDropDown: measureInWindow failed", error);
}
}
}, [triggerLayout?.height, triggerLayout?.width]);
const normalizedProjects = Array.isArray(projects) ? projects : []; const normalizedProjects = Array.isArray(projects) ? projects : [];
const isDisabled = normalizedProjects.length === 0; const isDisabled = normalizedProjects.length === 0;
@@ -80,8 +100,11 @@ const ProjectDropDown = ({
if (isDisabled) { if (isDisabled) {
return; return;
} }
if (!isOpen) {
measureTriggerPosition();
}
setIsOpen((prev) => !prev); setIsOpen((prev) => !prev);
}, [isDisabled]); }, [isDisabled, isOpen, measureTriggerPosition]);
const closeDropdown = useCallback(() => setIsOpen(false), []); const closeDropdown = useCallback(() => setIsOpen(false), []);
@@ -121,6 +144,12 @@ const ProjectDropDown = ({
}).start(); }).start();
}, [dropdownAnimation, isOpen]); }, [dropdownAnimation, isOpen]);
useEffect(() => {
if (isOpen) {
measureTriggerPosition();
}
}, [isOpen, measureTriggerPosition]);
const overlayTranslateY = useMemo( const overlayTranslateY = useMemo(
() => () =>
dropdownAnimation.interpolate({ dropdownAnimation.interpolate({
@@ -158,58 +187,29 @@ const ProjectDropDown = ({
[dropdownAnimation] [dropdownAnimation]
); );
return ( const overlayPositionStyle = useMemo(() => {
<View style={[styles.container, style]}> const widthCandidate =
{isOpen ? ( dropdownWidth ||
<Pressable style={styles.backdrop} onPress={closeDropdown} /> triggerWindowLayout?.width ||
) : null} triggerLayout?.width ||
0;
<View const width = widthCandidate || 0;
style={styles.triggerWrapper} const windowWidth = Dimensions.get("window").width || 0;
onLayout={(event) => setTriggerLayout(event.nativeEvent.layout)} const leftBase = triggerWindowLayout?.x ?? 0;
> const left =
<Animated.View width > 0 && windowWidth > 0
pointerEvents={isOpen ? "none" : "auto"} ? Math.min(Math.max(leftBase, 0), Math.max(0, windowWidth - width))
style={[ : Math.max(leftBase, 0);
styles.dropdownTriggerContainer,
{
opacity: triggerOpacity,
transform: [{ translateY: triggerTranslateY }],
},
]}
>
<BlurView
intensity={BUTTON_BLUR_INTENSITY}
tint="dark"
style={[
styles.dropdownBlur,
isDisabled && styles.dropdownBlurDisabled,
]}
>
<ProjectRow
isTitle={true}
isOpen={isOpen}
project={currentProject}
formatDate={formatDate}
onSelect={toggleDropdown}
/>
</BlurView>
</Animated.View>
<Animated.View return {
pointerEvents={isOpen ? "auto" : "none"} top: triggerWindowLayout?.y ?? 0,
style={[ left,
styles.dropdownOverlayContainer, width: width || undefined,
{ width: dropdownWidth || "100%" }, };
{ }, [dropdownWidth, triggerLayout?.width, triggerWindowLayout]);
opacity: dropdownAnimation,
transform: [ const dropdownContent = (
{ translateY: overlayTranslateY },
{ scale: overlayScale },
],
},
]}
>
<BlurView <BlurView
intensity={BUTTON_BLUR_INTENSITY} intensity={BUTTON_BLUR_INTENSITY}
tint="dark" tint="dark"
@@ -239,6 +239,7 @@ const ProjectDropDown = ({
onModify={handleModify} onModify={handleModify}
/> />
)} )}
nestedScrollEnabled
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={styles.dropdownListContent} contentContainerStyle={styles.dropdownListContent}
style={styles.dropdownList} style={styles.dropdownList}
@@ -250,7 +251,69 @@ const ProjectDropDown = ({
/> />
</View> </View>
</BlurView> </BlurView>
);
return (
<View style={[styles.container, style]}>
<View
ref={triggerRef}
style={styles.triggerWrapper}
onLayout={(event) => {
setTriggerLayout(event.nativeEvent.layout);
measureTriggerPosition();
}}
>
<Animated.View
pointerEvents={isOpen ? "none" : "auto"}
style={[
styles.dropdownTriggerContainer,
{
opacity: triggerOpacity,
transform: [{ translateY: triggerTranslateY }],
},
]}
>
<BlurView
intensity={BUTTON_BLUR_INTENSITY}
tint="dark"
style={[
styles.dropdownBlur,
isDisabled && styles.dropdownBlurDisabled,
]}
>
<ProjectRow
isTitle={true}
isOpen={isOpen}
project={currentProject}
formatDate={formatDate}
onSelect={toggleDropdown}
/>
</BlurView>
</Animated.View> </Animated.View>
{isOpen && triggerWindowLayout ? (
<Portal>
<View style={styles.portalContainer}>
<Pressable style={styles.backdrop} onPress={closeDropdown} />
<Animated.View
pointerEvents="auto"
style={[
styles.dropdownOverlayContainer,
overlayPositionStyle,
{
opacity: dropdownAnimation,
transform: [
{ translateY: overlayTranslateY },
{ scale: overlayScale },
],
},
]}
>
{dropdownContent}
</Animated.View>
</View>
</Portal>
) : null}
</View> </View>
</View> </View>
); );
@@ -435,6 +498,11 @@ const styles = StyleSheet.create({
chevronOpen: { chevronOpen: {
transform: [{ rotate: "180deg" }], transform: [{ rotate: "180deg" }],
}, },
portalContainer: {
...StyleSheet.absoluteFillObject,
zIndex: 999,
elevation: 999,
},
backdrop: { backdrop: {
...StyleSheet.absoluteFillObject, ...StyleSheet.absoluteFillObject,
zIndex: 1, zIndex: 1,