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:
@@ -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.",
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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>'
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user