Update README with HTML structure and scripts#539
Update README with HTML structure and scripts#539lemassade-hash wants to merge 1 commit intofirebase:mainfrom
Conversation
<!DOCTYPE html><html lang="fr"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Transmissions Foyer PRO</title> <script src="https://cdn.tailwindcss.com"></script> <!-- Firebase --> <script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-app-compat.js"></script> <script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-firestore-compat.js"></script> <script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-auth-compat.js"></script></head> <body class="bg-gray-100 p-4"><div class="max-w-4xl mx-auto"> <h1 class="text-2xl font-bold mb-4">📋 Transmissions - Foyer (Dossier jeune)</h1> <!-- LOGIN --> <div id="login" class="bg-white p-4 rounded-2xl shadow mb-4"> <input id="email" placeholder="Email" class="border p-2 w-full mb-2 rounded" /> <input id="password" type="password" placeholder="Mot de passe" class="border p-2 w-full mb-2 rounded" /> <button onclick="login()" class="bg-blue-500 text-white px-4 py-2 rounded-2xl w-full">Connexion</button> </div> <!-- APP --> <div id="app" class="hidden"> <button onclick="logout()" class="bg-red-500 text-white px-4 py-2 rounded-2xl mb-4">Déconnexion</button><!-- SELECTION JEUNE --> <select id="selectJeune" onchange="showJeune()" class="border p-2 w-full mb-4 rounded"></select> <!-- DOSSIER JEUNE --> <div id="ficheJeune" class="bg-white p-4 rounded-xl shadow mb-4 hidden"> <h2 id="nomJeune" class="text-xl font-bold mb-2"></h2> <div id="statsJeune" class="text-sm text-gray-600"></div> </div> <button onclick="toggleForm()" class="bg-blue-500 text-white px-4 py-2 rounded-2xl mb-4"> ➕ Nouvelle transmission </button> <div id="form" class="hidden bg-white p-4 rounded-2xl shadow mb-4"> <input id="jeune" placeholder="Nom du jeune" class="border p-2 w-full mb-2 rounded" /> <input id="educ" placeholder="Éducateur" class="border p-2 w-full mb-2 rounded" /> <select id="type" class="border p-2 w-full mb-2 rounded"> <option>Info</option> <option>Incident</option> <option>Médical</option> <option>Comportement</option> </select> <textarea id="message" placeholder="Transmission..." class="border p-2 w-full mb-2 rounded"></textarea> <button onclick="addTransmission()" class="bg-green-500 text-white px-4 py-2 rounded-2xl"> ✅ Enregistrer </button> </div> <div id="list" class="space-y-2"></div> </div> </div><script> const firebaseConfig = { apiKey: "TON_API_KEY", authDomain: "TON_PROJECT.firebaseapp.com", projectId: "TON_PROJECT_ID", }; firebase.initializeApp(firebaseConfig); const db = firebase.firestore(); const auth = firebase.auth(); let allData = []; let currentJeune = null; function login() { const email = document.getElementById('email').value; const password = document.getElementById('password').value; auth.signInWithEmailAndPassword(email, password) .catch(err => alert(err.message)); } function logout() { auth.signOut(); } auth.onAuthStateChanged(user => { if (user) { document.getElementById('login').classList.add('hidden'); document.getElementById('app').classList.remove('hidden'); loadData(); } else { document.getElementById('login').classList.remove('hidden'); document.getElementById('app').classList.add('hidden'); } }); function toggleForm() { document.getElementById('form').classList.toggle('hidden'); } function addTransmission() { const jeune = document.getElementById('jeune').value; const educ = document.getElementById('educ').value; const type = document.getElementById('type').value; const message = document.getElementById('message').value; if (!jeune || !educ || !message) { alert("Merci de remplir tous les champs"); return; } db.collection("transmissions").add({ jeune, educ, type, message, date: new Date() }); document.getElementById('jeune').value = ''; document.getElementById('educ').value = ''; document.getElementById('message').value = ''; } function populateJeunes() { const select = document.getElementById('selectJeune'); const names = [...new Set(allData.map(d => d.data().jeune))]; select.innerHTML = '<option value="">-- Choisir un jeune --</option>'; names.forEach(name => { const opt = document.createElement('option'); opt.value = name; opt.innerText = name; select.appendChild(opt); }); } function showJeune() { const name = document.getElementById('selectJeune').value; currentJeune = name; if (!name) return; const filtered = allData.filter(d => d.data().jeune === name); document.getElementById('ficheJeune').classList.remove('hidden'); document.getElementById('nomJeune').innerText = name; let incidents = 0; filtered.forEach(d => { if (d.data().type === "Incident") incidents++; }); document.getElementById('statsJeune').innerText = `Total : ${filtered.length} | Incidents : ${incidents}`; render(filtered); } function render(data) { const list = document.getElementById('list'); list.innerHTML = ''; data.forEach(doc => { const t = doc.data(); const div = document.createElement('div'); div.className = 'bg-white p-3 rounded-2xl shadow'; div.innerHTML = ` <div class="text-sm text-gray-500">${new Date(t.date.seconds*1000).toLocaleString()}</div> <div><strong>${t.jeune}</strong> - ${t.type}</div> <div class="text-sm">👤 ${t.educ}</div> <div class="mt-2">${t.message}</div> `; list.appendChild(div); }); } function loadData() { db.collection("transmissions").orderBy("date", "desc") .onSnapshot(snapshot => { allData = snapshot.docs; populateJeunes(); render(allData); }); } </script></body> </html>
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request adds a complete HTML and JavaScript application for managing transmissions directly into the README.md file. The review identifies several critical issues, including a Cross-Site Scripting (XSS) vulnerability due to unsafe innerHTML usage, a logic bug where real-time updates reset the UI state, and potential runtime errors related to timestamp handling. It is recommended to move the application to a separate file and implement the suggested improvements for data sanitization and integrity.
| div.innerHTML = ` | ||
| <div class="text-sm text-gray-500">${new Date(t.date.seconds*1000).toLocaleString()}</div> | ||
| <div><strong>${t.jeune}</strong> - ${t.type}</div> | ||
| <div class="text-sm">👤 ${t.educ}</div> | ||
| <div class="mt-2">${t.message}</div> | ||
| `; |
There was a problem hiding this comment.
The use of innerHTML to render user-provided data such as t.jeune, t.type, t.educ, and t.message introduces a significant Cross-Site Scripting (XSS) vulnerability. An attacker could inject malicious scripts into these fields which would then execute in the context of other users' browsers. Use textContent or a dedicated sanitization library to safely display user-generated content.
| <!DOCTYPE html><html lang="fr"> | ||
| <head> | ||
| <meta charset="UTF-8" /> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||
| <title>Transmissions Foyer PRO</title> | ||
| <script src="https://cdn.tailwindcss.com"></script> <!-- Firebase --> <script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-app-compat.js"></script> <script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-firestore-compat.js"></script> <script src="https://www.gstatic.com/firebasejs/9.22.2/firebase-auth-compat.js"></script></head> | ||
| <body class="bg-gray-100 p-4"><div class="max-w-4xl mx-auto"> | ||
| <h1 class="text-2xl font-bold mb-4">📋 Transmissions - Foyer (Dossier jeune)</h1> <!-- LOGIN --> <div id="login" class="bg-white p-4 rounded-2xl shadow mb-4"> | ||
| <input id="email" placeholder="Email" class="border p-2 w-full mb-2 rounded" /> | ||
| <input id="password" type="password" placeholder="Mot de passe" class="border p-2 w-full mb-2 rounded" /> | ||
| <button onclick="login()" class="bg-blue-500 text-white px-4 py-2 rounded-2xl w-full">Connexion</button> | ||
| </div> <!-- APP --> <div id="app" class="hidden"> | ||
| <button onclick="logout()" class="bg-red-500 text-white px-4 py-2 rounded-2xl mb-4">Déconnexion</button><!-- SELECTION JEUNE --> | ||
| <select id="selectJeune" onchange="showJeune()" class="border p-2 w-full mb-4 rounded"></select> | ||
|
|
||
| <!-- DOSSIER JEUNE --> | ||
| <div id="ficheJeune" class="bg-white p-4 rounded-xl shadow mb-4 hidden"> | ||
| <h2 id="nomJeune" class="text-xl font-bold mb-2"></h2> | ||
| <div id="statsJeune" class="text-sm text-gray-600"></div> | ||
| </div> | ||
|
|
||
| <button onclick="toggleForm()" class="bg-blue-500 text-white px-4 py-2 rounded-2xl mb-4"> | ||
| ➕ Nouvelle transmission | ||
| </button> | ||
|
|
||
| <div id="form" class="hidden bg-white p-4 rounded-2xl shadow mb-4"> | ||
| <input id="jeune" placeholder="Nom du jeune" class="border p-2 w-full mb-2 rounded" /> | ||
| <input id="educ" placeholder="Éducateur" class="border p-2 w-full mb-2 rounded" /> | ||
| <select id="type" class="border p-2 w-full mb-2 rounded"> | ||
| <option>Info</option> | ||
| <option>Incident</option> | ||
| <option>Médical</option> | ||
| <option>Comportement</option> | ||
| </select> | ||
| <textarea id="message" placeholder="Transmission..." class="border p-2 w-full mb-2 rounded"></textarea> | ||
| <button onclick="addTransmission()" class="bg-green-500 text-white px-4 py-2 rounded-2xl"> | ||
| ✅ Enregistrer | ||
| </button> | ||
| </div> | ||
|
|
||
| <div id="list" class="space-y-2"></div> | ||
|
|
||
| </div> | ||
| </div><script> | ||
| const firebaseConfig = { | ||
| apiKey: "TON_API_KEY", | ||
| authDomain: "TON_PROJECT.firebaseapp.com", | ||
| projectId: "TON_PROJECT_ID", | ||
| }; | ||
|
|
||
| firebase.initializeApp(firebaseConfig); | ||
| const db = firebase.firestore(); | ||
| const auth = firebase.auth(); | ||
|
|
||
| let allData = []; | ||
| let currentJeune = null; | ||
|
|
||
| function login() { | ||
| const email = document.getElementById('email').value; | ||
| const password = document.getElementById('password').value; | ||
| auth.signInWithEmailAndPassword(email, password) | ||
| .catch(err => alert(err.message)); | ||
| } | ||
|
|
||
| function logout() { | ||
| auth.signOut(); | ||
| } | ||
|
|
||
| auth.onAuthStateChanged(user => { | ||
| if (user) { | ||
| document.getElementById('login').classList.add('hidden'); | ||
| document.getElementById('app').classList.remove('hidden'); | ||
| loadData(); | ||
| } else { | ||
| document.getElementById('login').classList.remove('hidden'); | ||
| document.getElementById('app').classList.add('hidden'); | ||
| } | ||
| }); | ||
|
|
||
| function toggleForm() { | ||
| document.getElementById('form').classList.toggle('hidden'); | ||
| } | ||
|
|
||
| function addTransmission() { | ||
| const jeune = document.getElementById('jeune').value; | ||
| const educ = document.getElementById('educ').value; | ||
| const type = document.getElementById('type').value; | ||
| const message = document.getElementById('message').value; | ||
|
|
||
| if (!jeune || !educ || !message) { | ||
| alert("Merci de remplir tous les champs"); | ||
| return; | ||
| } | ||
|
|
||
| db.collection("transmissions").add({ | ||
| jeune, | ||
| educ, | ||
| type, | ||
| message, | ||
| date: new Date() | ||
| }); | ||
|
|
||
| document.getElementById('jeune').value = ''; | ||
| document.getElementById('educ').value = ''; | ||
| document.getElementById('message').value = ''; | ||
| } | ||
|
|
||
| function populateJeunes() { | ||
| const select = document.getElementById('selectJeune'); | ||
| const names = [...new Set(allData.map(d => d.data().jeune))]; | ||
|
|
||
| select.innerHTML = '<option value="">-- Choisir un jeune --</option>'; | ||
| names.forEach(name => { | ||
| const opt = document.createElement('option'); | ||
| opt.value = name; | ||
| opt.innerText = name; | ||
| select.appendChild(opt); | ||
| }); | ||
| } | ||
|
|
||
| function showJeune() { | ||
| const name = document.getElementById('selectJeune').value; | ||
| currentJeune = name; | ||
|
|
||
| if (!name) return; | ||
|
|
||
| const filtered = allData.filter(d => d.data().jeune === name); | ||
|
|
||
| document.getElementById('ficheJeune').classList.remove('hidden'); | ||
| document.getElementById('nomJeune').innerText = name; | ||
|
|
||
| let incidents = 0; | ||
| filtered.forEach(d => { | ||
| if (d.data().type === "Incident") incidents++; | ||
| }); | ||
|
|
||
| document.getElementById('statsJeune').innerText = | ||
| `Total : ${filtered.length} | Incidents : ${incidents}`; | ||
|
|
||
| render(filtered); | ||
| } | ||
|
|
||
| function render(data) { | ||
| const list = document.getElementById('list'); | ||
| list.innerHTML = ''; | ||
|
|
||
| data.forEach(doc => { | ||
| const t = doc.data(); | ||
| const div = document.createElement('div'); | ||
| div.className = 'bg-white p-3 rounded-2xl shadow'; | ||
| div.innerHTML = ` | ||
| <div class="text-sm text-gray-500">${new Date(t.date.seconds*1000).toLocaleString()}</div> | ||
| <div><strong>${t.jeune}</strong> - ${t.type}</div> | ||
| <div class="text-sm">👤 ${t.educ}</div> | ||
| <div class="mt-2">${t.message}</div> | ||
| `; | ||
| list.appendChild(div); | ||
| }); | ||
| } | ||
|
|
||
| function loadData() { | ||
| db.collection("transmissions").orderBy("date", "desc") | ||
| .onSnapshot(snapshot => { | ||
| allData = snapshot.docs; | ||
| populateJeunes(); | ||
| render(allData); | ||
| }); | ||
| } | ||
| </script></body> | ||
| </html> |
There was a problem hiding this comment.
The addition of a complete HTML/JavaScript application directly into the README.md file is unconventional and negatively impacts the maintainability and readability of the documentation. This repository is dedicated to "App Hosting adapters," and this specific application seems out of place. It is recommended to move this code to a separate file (e.g., index.html) or wrap it in a proper Markdown code block if it is intended as a usage example.
| educ, | ||
| type, | ||
| message, | ||
| date: new Date() |
There was a problem hiding this comment.
Using new Date() on the client side can lead to inconsistent timestamps due to variations in system clocks or user manipulation. It is recommended to use Firestore's server-side timestamp for better data integrity.
| date: new Date() | |
| date: firebase.firestore.FieldValue.serverTimestamp() |
| .onSnapshot(snapshot => { | ||
| allData = snapshot.docs; | ||
| populateJeunes(); | ||
| render(allData); |
There was a problem hiding this comment.
The onSnapshot listener currently calls render(allData) on every update, which will overwrite any filtered view the user might have selected via showJeune(). Additionally, populateJeunes() will reset the dropdown selection. The application should preserve the user's current selection and filter state when data updates.
| const div = document.createElement('div'); | ||
| div.className = 'bg-white p-3 rounded-2xl shadow'; | ||
| div.innerHTML = ` | ||
| <div class="text-sm text-gray-500">${new Date(t.date.seconds*1000).toLocaleString()}</div> |
There was a problem hiding this comment.
Accessing t.date.seconds without a null check can cause a runtime error when using Firestore's serverTimestamp. The field may be null in the local cache before the server-side value is synchronized. Add a check to handle the case where t.date is not yet available.
| <div class="text-sm text-gray-500">${new Date(t.date.seconds*1000).toLocaleString()}</div> | |
| <div class="text-sm text-gray-500">${t.date ? new Date(t.date.seconds*1000).toLocaleString() : '...'}</div> |
📋 Transmissions - Foyer (Dossier jeune)
firebase.initializeApp(firebaseConfig);
const db = firebase.firestore();
const auth = firebase.auth();
let allData = [];
let currentJeune = null;
function login() {
const email = document.getElementById('email').value;
const password = document.getElementById('password').value;
auth.signInWithEmailAndPassword(email, password)
.catch(err => alert(err.message));
}
function logout() {
auth.signOut();
}
auth.onAuthStateChanged(user => {
if (user) {
document.getElementById('login').classList.add('hidden');
document.getElementById('app').classList.remove('hidden');
loadData();
} else {
document.getElementById('login').classList.remove('hidden');
document.getElementById('app').classList.add('hidden');
}
});
function toggleForm() {
document.getElementById('form').classList.toggle('hidden');
}
function addTransmission() {
const jeune = document.getElementById('jeune').value;
const educ = document.getElementById('educ').value;
const type = document.getElementById('type').value;
const message = document.getElementById('message').value;
if (!jeune || !educ || !message) {
alert("Merci de remplir tous les champs");
return;
}
db.collection("transmissions").add({
jeune,
educ,
type,
message,
date: new Date()
});
document.getElementById('jeune').value = '';
document.getElementById('educ').value = '';
document.getElementById('message').value = '';
}
function populateJeunes() {
const select = document.getElementById('selectJeune');
const names = [...new Set(allData.map(d => d.data().jeune))];
select.innerHTML = '-- Choisir un jeune --';
names.forEach(name => {
const opt = document.createElement('option');
opt.value = name;
opt.innerText = name;
select.appendChild(opt);
});
}
function showJeune() {
const name = document.getElementById('selectJeune').value;
currentJeune = name;
if (!name) return;
const filtered = allData.filter(d => d.data().jeune === name);
document.getElementById('ficheJeune').classList.remove('hidden');
document.getElementById('nomJeune').innerText = name;
let incidents = 0;
filtered.forEach(d => {
if (d.data().type === "Incident") incidents++;
});
document.getElementById('statsJeune').innerText =
Total : ${filtered.length} | Incidents : ${incidents};render(filtered);
}
function render(data) {
const list = document.getElementById('list');
list.innerHTML = '';
data.forEach(doc => {
const t = doc.data();
const div = document.createElement('div');
div.className = 'bg-white p-3 rounded-2xl shadow';
div.innerHTML =
<div class="text-sm text-gray-500">${new Date(t.date.seconds*1000).toLocaleString()}</div> <div><strong>${t.jeune}</strong> - ${t.type}</div> <div class="text-sm">👤 ${t.educ}</div> <div class="mt-2">${t.message}</div>;list.appendChild(div);
});
}
function loadData() {
db.collection("transmissions").orderBy("date", "desc")
.onSnapshot(snapshot => {
allData = snapshot.docs;
populateJeunes();
render(allData);
});
}
</script>