Retour d'expérience PyCon 2026
++ Nous apprécions votre opinion. Dites-nous ce que vous avez aimé et comment nous pourrions nous améliorer. Votre réponse restera anonyme. +
+diff --git a/app/routers/router_2026.py b/app/routers/router_2026.py
index 8e761a5..7c4722f 100644
--- a/app/routers/router_2026.py
+++ b/app/routers/router_2026.py
@@ -33,6 +33,12 @@
grant_results_date = date(2026, 8, 15)
+feedback_open_at = datetime(2026, 8, 28, 12, 0, tzinfo=timezone.utc)
+
+
+def _feedback_is_open() -> bool:
+ return datetime.now(timezone.utc) >= feedback_open_at
+
PartnerType = Literal[
"partnership",
@@ -1668,9 +1674,19 @@ 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):
+ feedback_is_open = _feedback_is_open()
+ 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",
+ extra_context={
+ "feedback_is_open": feedback_is_open,
+ "feedback_open_at": feedback_open_at,
+ },
+ )
@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..292b487
--- /dev/null
+++ b/app/static/2026/css/pages/feedback.css
@@ -0,0 +1,145 @@
+.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; }
+.feedback-coming-soon-text {
+ color: #444;
+ font-size: 1.1rem;
+ line-height: 1.6;
+ margin: 0;
+ text-align: center;
+}
+.feedback-coming-soon-date {
+ color: #2D5016;
+ font-weight: 600;
+ font-size: 0.95rem;
+ margin: 16px 0 0;
+ text-align: center;
+}
+.feedback-form-wrapper.feedback-form-locked {
+ position: relative;
+ pointer-events: none;
+}
+.feedback-form-wrapper.feedback-form-locked .feedback-form * {
+ pointer-events: none;
+}
+.feedback-form-wrapper.feedback-form-locked .feedback-form .feedback-status {
+ pointer-events: auto;
+}
+.feedback-closed-banner {
+ margin-bottom: 20px;
+ padding: 16px 20px;
+ border-radius: 10px;
+ border: 2px solid rgba(255, 0, 0, 0.12);
+ background: rgba(255, 0, 0, 0.04);
+ color: #800;
+}
+.feedback-closed-banner strong {
+ font-weight: 700;
+ display: block;
+ margin-bottom: 4px;
+}
+@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..00b63d6
--- /dev/null
+++ b/app/static/2026/js/feedback.js
@@ -0,0 +1,148 @@
+(function() {
+ "use strict";
+
+ var form = document.getElementById("feedbackForm");
+ if (!form) return;
+
+ var apiUrl = "https://api.pycontg.pytogo.org/api/feedback/";
+ var submitBtn = document.getElementById("submitBtn");
+ var resetBtn = document.getElementById("resetBtn");
+ var statusEl = document.getElementById("formStatus");
+ var statusMsg = statusEl.querySelector(".feedback-status-msg");
+ var feedbackIsOpen = form.dataset.feedbackOpen === "true";
+
+ var i18n = {
+ fr: {
+ required: "Veuillez remplir les champs requis.",
+ days_required: "Veuillez sélectionner au moins un jour.",
+ not_open: "Le formulaire n'est pas encore ouvert.",
+ 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.",
+ not_open: "The feedback form is not yet open.",
+ 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() {
+ var docLang = (document.documentElement.lang || "fr").toLowerCase();
+ return docLang.startsWith("en") ? "en" : (docLang.startsWith("fr") ? "fr" : "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 lang = getLang();
+ var t = i18n[lang] || i18n.en;
+
+ if (!feedbackIsOpen) {
+ showStatus(false, t.not_open);
+ return;
+ }
+
+ var fd = new FormData(form);
+
+ 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..fed688c
--- /dev/null
+++ b/app/templates/2026/2026_feedback.html
@@ -0,0 +1,183 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+ Nous apprécions votre opinion. Dites-nous ce que vous avez aimé et comment nous pourrions nous améliorer. Votre réponse restera anonyme.
+ Retour d'expérience PyCon 2026
+