feat(feedback): pill button, umami ID capture, contact field, migration

- Button restyled from round icon-only to pill with speech-bubble icon + "Feedback" text
- Hidden umami_id field populated from localStorage.getItem('umami.uuid')
- Optional contact field (email/name) shown to anonymous users only
- Migration 0016 adds umami_id and contact columns to feedback table
- Route saves all three new fields (user_id was already captured)
- conftest.py: patch_config now sets WAITLIST_MODE=False to isolate tests from env

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Deeman
2026-02-20 21:10:02 +01:00
parent 23bed0d5f9
commit 24f528a157
5 changed files with 27 additions and 5 deletions

View File

@@ -43,6 +43,7 @@ _TRANSLATIONS: dict[str, dict[str, str]] = {
"base_manage_cookies": "Manage Cookies", "base_manage_cookies": "Manage Cookies",
"base_about": "About", "base_about": "About",
"base_feedback_placeholder": "Ideas to improve this page...", "base_feedback_placeholder": "Ideas to improve this page...",
"base_feedback_contact_placeholder": "Your email (optional)",
# ── Cookie banner ──────────────────────────────────────────────────── # ── Cookie banner ────────────────────────────────────────────────────
"cookie_title": "Cookie Preferences", "cookie_title": "Cookie Preferences",
"cookie_message": "We use cookies to keep you signed in and improve the site.", "cookie_message": "We use cookies to keep you signed in and improve the site.",
@@ -850,6 +851,7 @@ _TRANSLATIONS: dict[str, dict[str, str]] = {
"base_manage_cookies": "Cookie-Einstellungen", "base_manage_cookies": "Cookie-Einstellungen",
"base_about": "\u00dcber uns", "base_about": "\u00dcber uns",
"base_feedback_placeholder": "Ideen zur Verbesserung dieser Seite\u2026", "base_feedback_placeholder": "Ideen zur Verbesserung dieser Seite\u2026",
"base_feedback_contact_placeholder": "Deine E-Mail (optional)",
# ── Cookie banner ──────────────────────────────────────────────────── # ── Cookie banner ────────────────────────────────────────────────────
"cookie_title": "Cookie-Einstellungen", "cookie_title": "Cookie-Einstellungen",
"cookie_message": "Wir verwenden Cookies, damit du angemeldet bleibst und die Website verbessert werden kann.", "cookie_message": "Wir verwenden Cookies, damit du angemeldet bleibst und die Website verbessert werden kann.",

View File

@@ -0,0 +1,6 @@
"""Add umami_id and contact columns to feedback table."""
def up(conn):
conn.execute("ALTER TABLE feedback ADD COLUMN umami_id TEXT")
conn.execute("ALTER TABLE feedback ADD COLUMN contact TEXT")

View File

@@ -126,10 +126,12 @@ async def feedback():
page_url = form.get("page_url", "") page_url = form.get("page_url", "")
user_id = session.get("user_id") user_id = session.get("user_id")
umami_id = form.get("umami_id", "") or None
contact = form.get("contact", "").strip() or None
await execute( await execute(
"INSERT INTO feedback (user_id, page_url, message) VALUES (?, ?, ?)", "INSERT INTO feedback (user_id, page_url, message, umami_id, contact) VALUES (?, ?, ?, ?, ?)",
(user_id, page_url, message), (user_id, page_url, message, umami_id, contact),
) )
return f'<p style="font-size:0.8125rem;color:#16A34A;padding:12px;text-align:center;font-weight:600">{t["flash_feedback_success"]}</p>' return f'<p style="font-size:0.8125rem;color:#16A34A;padding:12px;text-align:center;font-weight:600">{t["flash_feedback_success"]}</p>'

View File

@@ -237,25 +237,36 @@
<button type="button" id="feedback-toggle" <button type="button" id="feedback-toggle"
aria-label="{{ t.nav_feedback }}" aria-label="{{ t.nav_feedback }}"
onclick="(function(){var p=document.getElementById('feedback-popover');p.hidden=!p.hidden;})()" onclick="(function(){var p=document.getElementById('feedback-popover');p.hidden=!p.hidden;})()"
style="width:48px;height:48px;border-radius:50%;background:#1D4ED8;border:none;cursor:pointer;box-shadow:0 4px 16px rgba(29,78,216,0.35);display:flex;align-items:center;justify-content:center;padding:0;"> style="height:38px;border-radius:999px;background:#1D4ED8;border:none;cursor:pointer;box-shadow:0 4px 16px rgba(29,78,216,0.35);display:inline-flex;align-items:center;gap:6px;padding:0 14px 0 10px;color:#fff;font-size:0.8125rem;font-weight:600;font-family:inherit;white-space:nowrap;">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" style="flex-shrink:0;">
<path d="M21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17H7L3 21V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V15Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M21 15C21 15.5304 20.7893 16.0391 20.4142 16.4142C20.0391 16.7893 19.5304 17 19 17H7L3 21V5C3 4.46957 3.21071 3.96086 3.58579 3.58579C3.96086 3.21071 4.46957 3 5 3H19C19.5304 3 20.0391 3.21071 20.4142 3.58579C20.7893 3.96086 21 4.46957 21 5V15Z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
{{ t.nav_feedback }}
</button> </button>
<div id="feedback-popover" hidden <div id="feedback-popover" hidden
style="position:absolute;bottom:calc(100% + 0.75rem);right:0;width:288px;background:white;border:1px solid #E2E8F0;border-radius:12px;padding:1rem;box-shadow:0 8px 32px rgba(0,0,0,0.12);"> style="position:absolute;bottom:calc(100% + 0.75rem);right:0;width:300px;background:white;border:1px solid #E2E8F0;border-radius:12px;padding:1rem;box-shadow:0 8px 32px rgba(0,0,0,0.12);">
<form hx-post="{{ url_for('public.feedback') }}" hx-target="#feedback-popover" hx-swap="innerHTML"> <form hx-post="{{ url_for('public.feedback') }}" hx-target="#feedback-popover" hx-swap="innerHTML">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"> <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="hidden" name="page_url" id="feedback-page-url"> <input type="hidden" name="page_url" id="feedback-page-url">
<input type="hidden" name="umami_id" id="feedback-umami-id">
<p style="font-size:0.8125rem;font-weight:600;color:#1E293B;margin:0 0 8px">{{ t.nav_feedback }}</p> <p style="font-size:0.8125rem;font-weight:600;color:#1E293B;margin:0 0 8px">{{ t.nav_feedback }}</p>
<textarea name="message" rows="3" required placeholder="{{ t.base_feedback_placeholder }}" <textarea name="message" rows="3" required placeholder="{{ t.base_feedback_placeholder }}"
style="width:100%;box-sizing:border-box;border:1px solid #E2E8F0;border-radius:6px;padding:8px;font-size:0.8125rem;font-family:inherit;resize:vertical"></textarea> style="width:100%;box-sizing:border-box;border:1px solid #E2E8F0;border-radius:6px;padding:8px;font-size:0.8125rem;font-family:inherit;resize:vertical"></textarea>
{% if not g.get("user") %}
<input type="text" name="contact" placeholder="{{ t.base_feedback_contact_placeholder }}"
style="width:100%;box-sizing:border-box;border:1px solid #E2E8F0;border-radius:6px;padding:8px;font-size:0.8125rem;font-family:inherit;margin-top:6px">
{% endif %}
<button type="submit" class="btn" style="width:100%;margin-top:8px;font-size:0.8125rem;padding:8px">{{ t.nav_send }}</button> <button type="submit" class="btn" style="width:100%;margin-top:8px;font-size:0.8125rem;padding:8px">{{ t.nav_send }}</button>
</form> </form>
</div> </div>
</div> </div>
<script> <script>
document.getElementById('feedback-page-url').value = window.location.pathname; document.getElementById('feedback-page-url').value = window.location.pathname;
(function() {
var uid = null;
try { uid = localStorage.getItem('umami.uuid'); } catch(e) {}
if (uid) document.getElementById('feedback-umami-id').value = uid;
})();
document.addEventListener('click', function(e) { document.addEventListener('click', function(e) {
var wrap = document.getElementById('feedback-wrap'); var wrap = document.getElementById('feedback-wrap');
if (wrap && !wrap.contains(e.target)) { if (wrap && !wrap.contains(e.target)) {

View File

@@ -153,6 +153,7 @@ def patch_config():
"PADDLE_WEBHOOK_SECRET": "whsec_test_secret", "PADDLE_WEBHOOK_SECRET": "whsec_test_secret",
"BASE_URL": "http://localhost:5000", "BASE_URL": "http://localhost:5000",
"DEBUG": True, "DEBUG": True,
"WAITLIST_MODE": False,
} }
for key, val in test_values.items(): for key, val in test_values.items():
original_values[key] = getattr(core.config, key, None) original_values[key] = getattr(core.config, key, None)