functions
This commit is contained in:
+85
-37
@@ -13,49 +13,97 @@ function basicTemplate({ title = "", content = "", button = null }) {
|
||||
button?.label || "Ouvrir"
|
||||
}</a></p>`
|
||||
: "";
|
||||
return `<!doctype html><html><body style="font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.5;color:#111">
|
||||
<div style="max-width:640px;margin:40px auto;padding:24px;border:1px solid #eee;border-radius:12px">
|
||||
<div style="text-align:center;margin-bottom:20px">
|
||||
<img src="${musicLandLogoSrc}" alt="MusicLand" style="height:40px" />
|
||||
</div>
|
||||
<h2 style="margin:0 0 16px">${title}</h2>
|
||||
<div>${content}</div>
|
||||
${btn}
|
||||
<hr style="margin:24px 0;border:none;border-top:1px solid #eee" />
|
||||
<p style="color:#666;font-size:12px">minuit.app</p>
|
||||
</div>
|
||||
return `<!doctype html><html lang="fr" style="background-color:#0b0b10"><head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="supported-color-schemes" content="dark" />
|
||||
<style>
|
||||
:root { color-scheme: dark; supported-color-schemes: dark; }
|
||||
body { margin:0 !important; background-color:#0b0b10 !important; }
|
||||
body, .email-surface { background-color:#0b0b10 !important; color:#ffffff !important; }
|
||||
u + .body .email-surface { background-color:#0b0b10 !important; color:#ffffff !important; }
|
||||
u + .body .email-content,
|
||||
[data-ogsc] .email-content,
|
||||
[data-ogsb] .email-content { background-color:#151520 !important; color:#ffffff !important; }
|
||||
u + .body .email-content *,
|
||||
[data-ogsc] .email-content *,
|
||||
[data-ogsb] .email-content * { color:#ffffff !important; }
|
||||
</style>
|
||||
</head><body class="body" data-ogsc="dark" data-ogsb="dark" bgcolor="#0b0b10" style="margin:0;padding:0;background-color:#0b0b10!important;color:#ffffff!important;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.5">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" class="email-surface" style="border-collapse:collapse;background-color:#0b0b10">
|
||||
<tr>
|
||||
<td align="center" style="padding:40px 16px">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" bgcolor="#151520" class="email-content" style="max-width:640px;border-radius:12px;background-color:#151520;border:1px solid rgba(255,255,255,0.08);box-shadow:0 12px 40px rgba(0,0,0,0.35);overflow:hidden">
|
||||
<tr>
|
||||
<td bgcolor="#151520" style="padding:24px;background-color:#151520;color:#ffffff">
|
||||
<div style="text-align:center;margin-bottom:20px;color:#ffffff">
|
||||
<img src="${musicLandLogoSrc}" alt="MusicLand" style="height:40px" />
|
||||
</div>
|
||||
<h2 style="margin:0 0 16px;font-size:24px;color:#ffffff!important">${title}</h2>
|
||||
<div style="color:rgba(255,255,255,0.9)!important">${content}</div>
|
||||
${btn}
|
||||
<hr style="margin:24px 0;border:none;border-top:1px solid rgba(255,255,255,0.1)" />
|
||||
<p style="color:rgba(255,255,255,0.45);font-size:12px;margin:0">minuit.app</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
function welcomeTemplate({ firstName = "", lastName = "" }) {
|
||||
const fullName = [firstName, lastName].filter(Boolean).join(" ").trim();
|
||||
const greeting = fullName ? `Salut ${fullName},` : "Salut,";
|
||||
return `<!doctype html><html><body style="margin:0;padding:0;background:#0b0b10;color:#fff;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif">
|
||||
<div style="max-width:640px;margin:0 auto;padding:40px 24px">
|
||||
<div style="background:#151520;border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,0.08)">
|
||||
<div style="padding:32px 28px">
|
||||
<div style="text-align:center;margin-bottom:24px">
|
||||
<img
|
||||
src="${musicLandLogoSrc}"
|
||||
alt="MusicLand"
|
||||
style="display:block;margin:0 auto;width:180px;max-width:100%;height:auto"
|
||||
/>
|
||||
</div>
|
||||
<p style="margin:0 0 12px;font-size:16px;letter-spacing:0.2px">${greeting}</p>
|
||||
<h1 style="margin:0 0 16px;font-size:28px;line-height:1.2">Bienvenue sur MusicLand</h1>
|
||||
<p style="margin:0 0 16px;font-size:16px;line-height:1.6;color:rgba(255,255,255,0.85)">
|
||||
Nous sommes ravis de t'accueillir dans la communauté des artistes et passionnés de MusicLand.
|
||||
</p>
|
||||
<p style="margin:0 0 16px;font-size:15px;line-height:1.6;color:rgba(255,255,255,0.75)">
|
||||
Explore les playlists collaboratives, dépose tes projets et découvre ce que prépare la scène émergente.
|
||||
</p>
|
||||
<p style="margin:0;font-size:15px;line-height:1.6;color:rgba(255,255,255,0.75)">
|
||||
L'équipe MusicLand
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="margin:32px 0 0;text-align:center;font-size:12px;color:rgba(255,255,255,0.45)">minuit.app · Paris, France</p>
|
||||
</div>
|
||||
return `<!doctype html><html lang="fr" style="background-color:#0b0b10"><head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="color-scheme" content="dark" />
|
||||
<meta name="supported-color-schemes" content="dark" />
|
||||
<style>
|
||||
:root { color-scheme: dark; supported-color-schemes: dark; }
|
||||
body { margin:0 !important; background-color:#0b0b10 !important; }
|
||||
body, .email-surface { background-color:#0b0b10 !important; color:#ffffff !important; }
|
||||
u + .body .email-surface { background-color:#0b0b10 !important; color:#ffffff !important; }
|
||||
u + .body .email-content,
|
||||
[data-ogsc] .email-content,
|
||||
[data-ogsb] .email-content { background-color:#151520 !important; color:#ffffff !important; }
|
||||
u + .body .email-content *,
|
||||
[data-ogsc] .email-content *,
|
||||
[data-ogsb] .email-content * { color:#ffffff !important; }
|
||||
</style>
|
||||
</head><body class="body" data-ogsc="dark" data-ogsb="dark" bgcolor="#0b0b10" style="margin:0;padding:0;background-color:#0b0b10!important;color:#ffffff!important;font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;line-height:1.5">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" class="email-surface" style="border-collapse:collapse;background-color:#0b0b10">
|
||||
<tr>
|
||||
<td align="center" style="padding:40px 16px">
|
||||
<table role="presentation" cellpadding="0" cellspacing="0" border="0" width="100%" bgcolor="#151520" class="email-content" style="max-width:640px;border-radius:16px;overflow:hidden;background-color:#151520;border:1px solid rgba(255,255,255,0.08);box-shadow:0 12px 40px rgba(0,0,0,0.35)">
|
||||
<tr>
|
||||
<td bgcolor="#151520" style="padding:32px 28px;background-color:#151520;color:#ffffff">
|
||||
<div style="text-align:center;margin-bottom:24px;color:#ffffff">
|
||||
<img
|
||||
src="${musicLandLogoSrc}"
|
||||
alt="MusicLand"
|
||||
style="display:block;margin:0 auto;width:180px;max-width:100%;height:auto"
|
||||
/>
|
||||
</div>
|
||||
<p style="margin:0 0 12px;font-size:16px;letter-spacing:0.2px;color:#ffffff!important">${greeting}</p>
|
||||
<h1 style="margin:0 0 16px;font-size:28px;line-height:1.2;color:#ffffff!important">Bienvenue sur MusicLand</h1>
|
||||
<p style="margin:0 0 16px;font-size:16px;line-height:1.6;color:#ffffff!important">
|
||||
Nous sommes ravis de t'accueillir dans la communauté des artistes et passionnés de MusicLand.
|
||||
</p>
|
||||
<p style="margin:0 0 16px;font-size:15px;line-height:1.6;color:#ffffff!important">
|
||||
Explore les playlists collaboratives, dépose tes projets et découvre ce que prépare la scène émergente.
|
||||
</p>
|
||||
<p style="margin:0;font-size:15px;line-height:1.6;color:#ffffff!important">
|
||||
L'équipe MusicLand
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="margin:32px 0 0;text-align:center;font-size:12px;color:rgba(255,255,255,0.6)">minuit.app · Paris, France</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body></html>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
const admin = require("firebase-admin");
|
||||
const { BATCH_TYPE } = require("../config/types");
|
||||
|
||||
async function deleteFolder(path) {
|
||||
try {
|
||||
@@ -11,4 +12,77 @@ async function deleteFolder(path) {
|
||||
}
|
||||
}
|
||||
|
||||
async function batchFirestore({
|
||||
path = "", // ADD only
|
||||
docs = [], // not for ADD
|
||||
data = {}, // not for DELETE
|
||||
type = BATCH_TYPE.UPDATE,
|
||||
}) {
|
||||
try {
|
||||
if (docs?.length === 0 && type !== BATCH_TYPE.ADD) {
|
||||
console.log(`Aucun document trouvé dans la collection ${path}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Préparer des lots pour les opérations
|
||||
const batches = [];
|
||||
let currentBatch = admin.firestore().batch();
|
||||
let operationCounter = 0;
|
||||
|
||||
docs.forEach((doc) => {
|
||||
const ref =
|
||||
doc?.ref ||
|
||||
(typeof doc?.path === "string"
|
||||
? admin.firestore().doc(doc.path)
|
||||
: null);
|
||||
const payload =
|
||||
doc && typeof doc.data === "object" && doc.data !== null && !Array.isArray(doc.data)
|
||||
? doc.data
|
||||
: data;
|
||||
|
||||
if (type === BATCH_TYPE.ADD) {
|
||||
// Pour ADD, créer un nouveau document avec ID automatique
|
||||
const newDocRef = admin.firestore().collection(path).doc();
|
||||
currentBatch.set(newDocRef, data);
|
||||
} else if (type === BATCH_TYPE.UPDATE) {
|
||||
if (!ref) {
|
||||
throw new Error("batchFirestore UPDATE nécessite une référence de document.");
|
||||
}
|
||||
if (!payload || Object.keys(payload).length === 0) {
|
||||
throw new Error("batchFirestore UPDATE nécessite des données à écrire.");
|
||||
}
|
||||
currentBatch.set(ref, payload, { merge: true });
|
||||
} else if (type === BATCH_TYPE.DELETE) {
|
||||
if (!ref) {
|
||||
throw new Error("batchFirestore DELETE nécessite une référence de document.");
|
||||
}
|
||||
currentBatch.delete(ref);
|
||||
} else {
|
||||
throw new Error(`Opération non supportée : ${type}`);
|
||||
}
|
||||
operationCounter++;
|
||||
// Si le lot atteint la limite de 500 opérations, le sauvegarder et en créer un nouveau
|
||||
if (operationCounter === 500) {
|
||||
batches.push(currentBatch);
|
||||
currentBatch = admin.firestore().batch();
|
||||
operationCounter = 0;
|
||||
}
|
||||
});
|
||||
|
||||
// Ajouter le dernier lot si des opérations y sont présentes
|
||||
if (operationCounter > 0) {
|
||||
batches.push(currentBatch);
|
||||
}
|
||||
|
||||
// Exécuter tous les lots en parallèle
|
||||
await Promise.all(batches.map((batch) => batch.commit()));
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Erreur lors des opérations ${type} pour la collection ${path} :`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
exports.batchFirestore = batchFirestore;
|
||||
exports.deleteFolder = deleteFolder;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
const normalizeDate = (input) =>
|
||||
typeof input?.toDate === "function" ? input.toDate() : new Date(input);
|
||||
|
||||
const buildMonthKey = (timestamp) => {
|
||||
const date = normalizeDate(timestamp);
|
||||
const year = date.getUTCFullYear();
|
||||
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
||||
return `${year}-${month}`;
|
||||
};
|
||||
|
||||
const buildPreviousMonthContext = (referenceDate) => {
|
||||
const current = referenceDate ? new Date(referenceDate) : new Date();
|
||||
current.setUTCHours(0, 0, 0, 0);
|
||||
current.setUTCDate(1);
|
||||
|
||||
const target = new Date(current);
|
||||
target.setUTCMonth(target.getUTCMonth() - 1);
|
||||
|
||||
const year = target.getUTCFullYear();
|
||||
const monthIndex = target.getUTCMonth();
|
||||
const rangeStart = new Date(Date.UTC(year, monthIndex, 1, 0, 0, 0, 0));
|
||||
const rangeEnd = new Date(Date.UTC(year, monthIndex + 1, 0, 23, 59, 59, 999));
|
||||
const monthKey = buildMonthKey(rangeStart);
|
||||
|
||||
return {
|
||||
year,
|
||||
month: monthIndex + 1,
|
||||
monthKey,
|
||||
rangeStart,
|
||||
rangeEnd,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
buildMonthKey,
|
||||
buildPreviousMonthContext,
|
||||
};
|
||||
Reference in New Issue
Block a user