classement tout les mois
This commit is contained in:
@@ -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");
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user