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