Initial commit: Club DynaSQY

This commit is contained in:
2026-08-23 09:04:47 +00:00
commit 0875890dc7
145 changed files with 16947 additions and 0 deletions
+185
View File
@@ -0,0 +1,185 @@
const pool = require('../config/db');
const path = require('path');
const fs = require('fs');
exports.showPage = async (req, res) => {
try {
const { token } = req.params;
const reponse = req.query.reponse || null;
const [rows] = await pool.query(
`SELECT i.*, u.prenom AS invite_par_prenom, u.nom AS invite_par_nom, u.societe AS invite_par_societe
FROM invites i
LEFT JOIN users u ON u.id = i.invite_par_id
WHERE i.token = ?`,
[token]
);
if (rows.length === 0) {
return sendErrorPage(res, 'Lien invalide ou expiré.');
}
const guest = rows[0];
if (reponse && ['acceptee', 'refusee'].includes(reponse)) {
await pool.query('UPDATE invites SET statut=? WHERE id=?', [reponse, guest.id]);
guest.statut = reponse;
}
const dateStr = guest.date_club
? new Date(guest.date_club).toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' })
: '';
const statutLabels = { en_attente: 'En attente', acceptee: 'Acceptée', refusee: 'Refusée' };
const statutColors = { en_attente: '#f59e0b', acceptee: '#16a34a', refusee: '#dc2626' };
const html = `<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Invitation Dyna Biz</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#f3f4f6;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:20px;min-height:100vh;display:flex;align-items:center;justify-content:center}
.card{background:#fff;border-radius:16px;box-shadow:0 2px 12px rgba(0,0,0,.08);max-width:480px;width:100%;overflow:hidden}
.header{background:#1e3a5f;padding:24px;text-align:center}
.header h1{color:#fff;font-size:20px;margin:0}
.body{padding:28px 24px}
.body h2{font-size:18px;color:#333;margin-bottom:16px}
.body p{font-size:15px;color:#555;margin-bottom:12px;line-height:1.5}
.label{color:#888;font-size:13px}
.statut{display:inline-block;padding:4px 12px;border-radius:20px;font-size:14px;font-weight:bold;color:#fff;margin-top:4px}
.btn-group{display:flex;gap:12px;margin:24px 0;flex-wrap:wrap}
.btn{flex:1;min-width:120px;padding:14px 20px;border:none;border-radius:12px;font-size:16px;font-weight:bold;color:#fff;cursor:pointer;text-align:center;text-decoration:none;display:inline-block;transition:opacity .2s}
.btn:active{opacity:.8}
.btn-accept{background:#16a34a}
.btn-refuse{background:#dc2626}
.btn:disabled{opacity:.5;cursor:not-allowed}
.btn-upload{background:#1e3a5f;color:#fff;padding:10px 20px;border:none;border-radius:8px;cursor:pointer;font-size:14px;margin-top:8px}
.file-info{font-size:13px;color:#888;margin-top:6px}
.msg{background:#f0fdf4;border:1px solid #bbf7d0;border-radius:12px;padding:16px;text-align:center;margin:16px 0}
.msg.error{background:#fef2f2;border-color:#fecaca}
.msg.info{background:#eff6ff;border-color:#bfdbfe}
.photo-preview{max-width:200px;max-height:200px;border-radius:12px;margin-top:12px;display:block}
.link{color:#1e3a5f;text-decoration:underline;font-size:14px}
.hidden{display:none}
</style>
</head>
<body>
<div class="card">
<div class="header">
<h1>Dyna Biz Coignieres</h1>
</div>
<div class="body">
<h2>Bonjour ${guest.prenom} ${guest.nom} !</h2>
<p><strong>${guest.invite_par_prenom} ${guest.invite_par_nom}</strong>${guest.invite_par_societe ? ' (' + guest.invite_par_societe + ')' : ''} vous invite au club Dyna Biz Coignieres.</p>
${guest.date_club ? '<p><strong>Date :</strong> ' + dateStr + '</p>' : ''}
<p style="margin-top:16px;"><span class="label">Statut actuel :</span><br><span class="statut" style="background:${statutColors[guest.statut]}">${statutLabels[guest.statut]}</span></p>
${guest.statut === 'en_attente' ? `
<div class="btn-group">
<a href="/invitation/${token}?reponse=acceptee" class="btn btn-accept">Accepter</a>
<a href="/invitation/${token}?reponse=refusee" class="btn btn-refuse">Refuser</a>
</div>
` : `
<div class="msg${guest.statut === 'acceptee' ? '' : ' error'}">
${guest.statut === 'acceptee' ? 'Vous avez accepté l\'invitation. Merci !' : 'Vous avez refusé l\'invitation.'}
</div>
`}
${guest.statut === 'acceptee' ? `
<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0">
<h2>Ajouter une photo</h2>
<p>Vous pouvez ajouter une photo pour que les membres puissent vous reconnaître.</p>
<img src="${guest.photo_url || ''}" alt="Votre photo" class="photo-preview" style="${guest.photo_url ? '' : 'display:none'}">
<form id="photoForm" enctype="multipart/form-data">
<input type="file" name="photo" accept="image/*" capture="environment" id="photoInput" style="display:block;margin:12px 0;font-size:14px">
<button type="submit" class="btn-upload" id="uploadBtn">Envoyer la photo</button>
<p class="file-info" id="uploadMsg"></p>
</form>
<script>
document.getElementById('photoForm').addEventListener('submit', async function(e) {
e.preventDefault();
const input = document.getElementById('photoInput');
const btn = document.getElementById('uploadBtn');
const msg = document.getElementById('uploadMsg');
if (!input.files.length) { msg.textContent = 'Veuillez sélectionner une photo.'; return; }
btn.disabled = true; btn.textContent = 'Envoi...';
const fd = new FormData();
fd.append('photo', input.files[0]);
try {
const r = await fetch('/api/public/invitation/${token}/photo', { method: 'POST', body: fd });
if (!r.ok) throw new Error('Erreur');
const data = await r.json();
msg.textContent = 'Photo envoyée avec succès !';
msg.style.color = '#16a34a';
if (data.photo_url) {
const img = document.querySelector('.photo-preview');
if (img) { img.src = data.photo_url + '?t=' + Date.now(); img.style.display = 'block'; }
}
input.value = '';
} catch(err) {
msg.textContent = 'Erreur lors de l\\'envoi.';
msg.style.color = '#dc2626';
} finally {
btn.disabled = false; btn.textContent = 'Envoyer la photo';
}
});
</script>
` : ''}
</div>
</div>
</body>
</html>`;
res.send(html);
} catch (err) {
console.error(err);
sendErrorPage(res, 'Une erreur est survenue.');
}
};
exports.respond = async (req, res) => {
try {
const { token } = req.params;
const { reponse } = req.body;
if (!['acceptee', 'refusee'].includes(reponse)) {
return res.status(400).json({ error: 'Réponse invalide' });
}
const [rows] = await pool.query('SELECT id FROM invites WHERE token = ?', [token]);
if (rows.length === 0) return res.status(404).json({ error: 'Lien invalide' });
await pool.query('UPDATE invites SET statut=? WHERE id=?', [reponse, rows[0].id]);
res.json({ message: 'Réponse enregistrée' });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
exports.uploadPhoto = async (req, res) => {
try {
const { token } = req.params;
const [rows] = await pool.query('SELECT id, photo_url FROM invites WHERE token = ?', [token]);
if (rows.length === 0) return res.status(404).json({ error: 'Lien invalide' });
if (!req.file) return res.status(400).json({ error: 'Fichier requis' });
if (rows[0].photo_url) {
const oldPath = path.join(__dirname, '..', rows[0].photo_url);
if (fs.existsSync(oldPath)) fs.unlinkSync(oldPath);
}
const photoUrl = '/uploads/' + req.file.filename;
await pool.query('UPDATE invites SET photo_url=? WHERE id=?', [photoUrl, rows[0].id]);
res.json({ message: 'Photo enregistrée', photo_url: photoUrl });
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
};
function sendErrorPage(res, msg) {
res.status(404).send(`<!DOCTYPE html>
<html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Invitation Dyna Biz</title>
<style>body{font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f3f4f6;padding:20px}
.card{background:#fff;border-radius:16px;padding:32px;text-align:center;max-width:400px;box-shadow:0 2px 12px rgba(0,0,0,.08)}
.card h2{color:#333;margin:0 0 8px}.card p{color:#888;font-size:15px}</style>
</head><body><div class="card"><h2>Oups !</h2><p>${msg}</p></div></body></html>`);
}