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.algolia = require("./src/algolia");
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}`,
);
},
);
+128 -60
View File
@@ -1,4 +1,5 @@
import { BlurView } from "expo-blur";
import { Portal } from "@gorhom/portal";
import React, {
useCallback,
useEffect,
@@ -52,8 +53,27 @@ const ProjectDropDown = ({
}) => {
const [isOpen, setIsOpen] = useState(false);
const [triggerLayout, setTriggerLayout] = useState(null);
const [triggerWindowLayout, setTriggerWindowLayout] = useState(null);
const triggerRef = useRef(null);
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 isDisabled = normalizedProjects.length === 0;
@@ -80,8 +100,11 @@ const ProjectDropDown = ({
if (isDisabled) {
return;
}
if (!isOpen) {
measureTriggerPosition();
}
setIsOpen((prev) => !prev);
}, [isDisabled]);
}, [isDisabled, isOpen, measureTriggerPosition]);
const closeDropdown = useCallback(() => setIsOpen(false), []);
@@ -121,6 +144,12 @@ const ProjectDropDown = ({
}).start();
}, [dropdownAnimation, isOpen]);
useEffect(() => {
if (isOpen) {
measureTriggerPosition();
}
}, [isOpen, measureTriggerPosition]);
const overlayTranslateY = useMemo(
() =>
dropdownAnimation.interpolate({
@@ -158,15 +187,81 @@ const ProjectDropDown = ({
[dropdownAnimation]
);
const overlayPositionStyle = useMemo(() => {
const widthCandidate =
dropdownWidth ||
triggerWindowLayout?.width ||
triggerLayout?.width ||
0;
const width = widthCandidate || 0;
const windowWidth = Dimensions.get("window").width || 0;
const leftBase = triggerWindowLayout?.x ?? 0;
const left =
width > 0 && windowWidth > 0
? Math.min(Math.max(leftBase, 0), Math.max(0, windowWidth - width))
: Math.max(leftBase, 0);
return {
top: triggerWindowLayout?.y ?? 0,
left,
width: width || undefined,
};
}, [dropdownWidth, triggerLayout?.width, triggerWindowLayout]);
const dropdownContent = (
<BlurView
intensity={BUTTON_BLUR_INTENSITY}
tint="dark"
style={styles.dropdownOverlay}
>
<ProjectRow
isTitle={true}
isOpen={isOpen}
project={currentProject}
formatDate={formatDate}
onSelect={toggleDropdown}
/>
<View style={styles.dropdownOverlayContent}>
<FlatList
data={dropdownProjects}
keyExtractor={(project, index) =>
project?.id || project?.title || `project-${index}`
}
ItemSeparatorComponent={() => <View style={{ height: 10 }} />}
renderItem={({ item: project }) => (
<ProjectRow
project={project}
formatDate={formatDate}
isSelected={currentProject?.id === project?.id}
onSelect={handleSelect}
onModify={handleModify}
/>
)}
nestedScrollEnabled
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.dropdownListContent}
style={styles.dropdownList}
/>
<GradientButton
containerStyle={styles.createButtonContainer}
title="Commencer à créer"
onPress={handleCreate}
/>
</View>
</BlurView>
);
return (
<View style={[styles.container, style]}>
{isOpen ? (
<Pressable style={styles.backdrop} onPress={closeDropdown} />
) : null}
<View
ref={triggerRef}
style={styles.triggerWrapper}
onLayout={(event) => setTriggerLayout(event.nativeEvent.layout)}
onLayout={(event) => {
setTriggerLayout(event.nativeEvent.layout);
measureTriggerPosition();
}}
>
<Animated.View
pointerEvents={isOpen ? "none" : "auto"}
@@ -196,61 +291,29 @@ const ProjectDropDown = ({
</BlurView>
</Animated.View>
<Animated.View
pointerEvents={isOpen ? "auto" : "none"}
style={[
styles.dropdownOverlayContainer,
{ width: dropdownWidth || "100%" },
{
opacity: dropdownAnimation,
transform: [
{ translateY: overlayTranslateY },
{ scale: overlayScale },
],
},
]}
>
<BlurView
intensity={BUTTON_BLUR_INTENSITY}
tint="dark"
style={styles.dropdownOverlay}
>
<ProjectRow
isTitle={true}
isOpen={isOpen}
project={currentProject}
formatDate={formatDate}
onSelect={toggleDropdown}
/>
<View style={styles.dropdownOverlayContent}>
<FlatList
data={dropdownProjects}
keyExtractor={(project, index) =>
project?.id || project?.title || `project-${index}`
}
ItemSeparatorComponent={() => <View style={{ height: 10 }} />}
renderItem={({ item: project }) => (
<ProjectRow
project={project}
formatDate={formatDate}
isSelected={currentProject?.id === project?.id}
onSelect={handleSelect}
onModify={handleModify}
/>
)}
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.dropdownListContent}
style={styles.dropdownList}
/>
<GradientButton
containerStyle={styles.createButtonContainer}
title="Commencer à créer"
onPress={handleCreate}
/>
{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>
</BlurView>
</Animated.View>
</Portal>
) : null}
</View>
</View>
);
@@ -435,6 +498,11 @@ const styles = StyleSheet.create({
chevronOpen: {
transform: [{ rotate: "180deg" }],
},
portalContainer: {
...StyleSheet.absoluteFillObject,
zIndex: 999,
elevation: 999,
},
backdrop: {
...StyleSheet.absoluteFillObject,
zIndex: 1,