From dc989d556764e55314e00c1afdfb53225c2f1738 Mon Sep 17 00:00:00 2001 From: georgegale228 Date: Fri, 14 Aug 2026 21:22:56 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(feedback):=20cr=C3=A9ation=20du=20form?= =?UTF-8?q?ulaire=20de=20feedback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routers/router_2026.py | 11 +- app/static/2026/css/pages/feedback.css | 108 ++++++++++++++++ app/static/2026/js/feedback.js | 136 ++++++++++++++++++++ app/templates/2026/2026_feedback.html | 166 +++++++++++++++++++++++++ app/templates/2026/base.html | 5 + 5 files changed, 423 insertions(+), 3 deletions(-) create mode 100644 app/static/2026/css/pages/feedback.css create mode 100644 app/static/2026/js/feedback.js create mode 100644 app/templates/2026/2026_feedback.html diff --git a/app/routers/router_2026.py b/app/routers/router_2026.py index 8e761a5..b5b804d 100644 --- a/app/routers/router_2026.py +++ b/app/routers/router_2026.py @@ -1668,9 +1668,14 @@ def volunteers(request: Request): @router.get("/feedback") -def feedback(request: Request): - # TODO - create a feedback page with a form to submit feedback - pass +async def feedback(request: Request): + return await _render_page_with_event( + request=request, + name="2026_feedback.html", + active_page="feedback", + page_css="feedback.css", + page_title="PyCon Togo 2026 - Feedback", + ) @router.get("/shop") diff --git a/app/static/2026/css/pages/feedback.css b/app/static/2026/css/pages/feedback.css new file mode 100644 index 0000000..bbb8e75 --- /dev/null +++ b/app/static/2026/css/pages/feedback.css @@ -0,0 +1,108 @@ +.feedback-card { + max-width: 820px; + margin: 0 auto; + background: #fff; + border-radius: 16px; + padding: 32px; + box-shadow: 0 4px 24px rgba(0,0,0,0.06); +} +.feedback-header { margin-bottom: 24px; } +.feedback-title { + color: #056d1e; + font-size: 1.75rem; + font-weight: 700; + margin: 0 0 8px; +} +.feedback-subtitle { + color: #444; + font-size: 1rem; + margin: 0; + line-height: 1.5; +} +.feedback-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; +} +.feedback-full { grid-column: 1 / -1; } +.feedback-field { display: flex; flex-direction: column; gap: 6px; } +.feedback-label { + font-weight: 600; + color: #2D5016; + font-size: 0.9rem; + margin-bottom: 4px; +} +.feedback-form input[type="text"], +.feedback-form input[type="number"], +.feedback-form select, +.feedback-form textarea { + width: 100%; + padding: 10px 12px; + border-radius: 8px; + border: 1px solid rgba(0,0,0,0.08); + background: #ffffff; + color: #111; + font-size: 0.95rem; + font-family: inherit; +} +.feedback-form textarea { min-height: 100px; resize: vertical; } +.feedback-radio-group { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } +.feedback-radio-item { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(5,109,30,0.04); + border: 1px solid rgba(0,0,0,0.05); + color: #056d1e; + font-size: 0.9rem; + cursor: pointer; +} +.feedback-radio-item input { margin-right: 4px; } +.feedback-checkbox-group { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } +.feedback-checkbox-item { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 12px; + border-radius: 999px; + background: rgba(5,109,30,0.04); + border: 1px solid rgba(0,0,0,0.05); + color: #056d1e; + font-size: 0.9rem; + cursor: pointer; +} +.feedback-checkbox-item input { margin-right: 4px; } +.feedback-actions { + display: flex; + gap: 12px; + align-items: center; + margin-top: 20px; + grid-column: 1 / -1; +} +.feedback-status { + grid-column: 1 / -1; + margin-top: 16px; + padding: 12px 16px; + border-radius: 10px; + display: none; + align-items: center; + gap: 10px; + font-weight: 600; +} +.feedback-status.success { + background: rgba(5,109,30,0.06); + border: 2px solid rgba(255,212,59,0.18); + color: #2D5016; +} +.feedback-status.error { + background: rgba(255,0,0,0.04); + border: 2px solid rgba(255,0,0,0.12); + color: #800; +} +.feedback-status-msg { flex: 1; } +@media (max-width: 720px) { + .feedback-card { padding: 20px; } + .feedback-grid { grid-template-columns: 1fr; } +} diff --git a/app/static/2026/js/feedback.js b/app/static/2026/js/feedback.js new file mode 100644 index 0000000..19d1773 --- /dev/null +++ b/app/static/2026/js/feedback.js @@ -0,0 +1,136 @@ +(function() { + "use strict"; + + var apiUrl = "https://api.pycontg.pytogo.org/api/feedback/"; + var form = document.getElementById("feedbackForm"); + var submitBtn = document.getElementById("submitBtn"); + var resetBtn = document.getElementById("resetBtn"); + var statusEl = document.getElementById("formStatus"); + var statusMsg = statusEl.querySelector(".feedback-status-msg"); + + var i18n = { + fr: { + required: "Veuillez remplir les champs requis.", + days_required: "Veuillez sélectionner au moins un jour.", + sending: "Envoi…", + success: "Merci ! Votre retour a été envoyé.", + error: "Une erreur est survenue. Veuillez réessayer plus tard.", + network: "Erreur réseau. Veuillez vérifier votre connexion." + }, + en: { + required: "Please fill in the required fields.", + days_required: "Please select at least one day.", + sending: "Sending…", + success: "Thank you! Your feedback has been sent.", + error: "An error occurred. Please try again later.", + network: "Network error. Please check your connection." + } + }; + + function getLang() { + return (window.currentLang || "fr").toLowerCase().startsWith("en") ? "en" : "fr"; + } + + function showStatus(success, text) { + statusEl.className = "feedback-status " + (success ? "success" : "error"); + statusMsg.textContent = text; + statusEl.style.display = "flex"; + } + + function hideStatus() { + statusEl.style.display = "none"; + statusMsg.textContent = ""; + } + + function setDaysError(show) { + var daysError = document.querySelector(".feedback-checkbox-error"); + if (!daysError) return; + daysError.style.display = show ? "block" : "none"; + } + + function daysSelected(fd) { + return fd.getAll("days").length > 0; + } + + var dayCheckboxes = document.querySelectorAll('input[type="checkbox"][name="days"]'); + dayCheckboxes.forEach(function(cb) { + cb.addEventListener("change", function() { + if (daysSelected(new FormData(form))) setDaysError(false); + }); + }); + + if (resetBtn) { + resetBtn.addEventListener("click", function() { + form.reset(); + setDaysError(false); + hideStatus(); + }); + } + + form.addEventListener("submit", async function(e) { + e.preventDefault(); + hideStatus(); + + var fd = new FormData(form); + var lang = getLang(); + var t = i18n[lang] || i18n.en; + + var payload = { + sex: fd.get("sex") || null, + age: fd.get("age") || null, + profession: (fd.get("profession") || "").trim() || null, + country: (fd.get("country") || "").trim() || null, + python_level: fd.get("python_level") || null, + days: fd.getAll("days"), + heard: fd.get("heard") || null, + rating: fd.get("rating") ? Number(fd.get("rating")) : null, + overall: (fd.get("overall") || "").trim() || null, + favorite: (fd.get("favorite") || "").trim() || null, + improvements: (fd.get("improvements") || "").trim() || null, + comments: (fd.get("comments") || "").trim() || null + }; + + if (!payload.days || payload.days.length === 0) { + setDaysError(true); + showStatus(false, t.days_required || t.required); + return; + } else { + setDaysError(false); + } + + if (!payload.sex && !payload.age && !payload.profession && !payload.heard && !payload.overall && !payload.favorite) { + showStatus(false, t.required); + return; + } + + submitBtn.disabled = true; + var originalText = submitBtn.textContent; + submitBtn.textContent = t.sending; + + try { + var res = await fetch(apiUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload) + }); + + if (res.ok) { + showStatus(true, t.success); + form.reset(); + setDaysError(false); + } else { + var msg = t.error; + try { + var data = await res.json(); + if (data && data.message) msg = data.message + " - " + msg; + } catch (err) {} + showStatus(false, msg); + } + } catch (err) { + showStatus(false, t.network); + } finally { + submitBtn.disabled = false; + submitBtn.textContent = originalText; + } + }); +})(); diff --git a/app/templates/2026/2026_feedback.html b/app/templates/2026/2026_feedback.html new file mode 100644 index 0000000..c4acb20 --- /dev/null +++ b/app/templates/2026/2026_feedback.html @@ -0,0 +1,166 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+ +
+
+
+ +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/2026/base.html b/app/templates/2026/base.html index 1e1d11e..6430905 100644 --- a/app/templates/2026/base.html +++ b/app/templates/2026/base.html @@ -76,6 +76,8 @@ data-i18n-fr="Speakers" href="/speakers">Speakers Schedule + Feedback diff --git a/app/templates/2026/base.html b/app/templates/2026/base.html index 6430905..a48b196 100644 --- a/app/templates/2026/base.html +++ b/app/templates/2026/base.html @@ -42,7 +42,7 @@ Home @@ -76,8 +78,6 @@ data-i18n-fr="Speakers" href="/speakers">Speakers Schedule - Feedback
@@ -176,8 +179,6 @@ onclick="closeMenu()">Our Speakers Schedule - Feedback
Register → @@ -249,6 +250,7 @@
Navigation
Sponsors Team Job Board + Feedback Contact