feat: feedback with photo support + web dashboard feedback button
All checks were successful
Security Checks / dependency-audit (push) Successful in 15s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 5s

iOS:
- Photo picker in feedback sheet (screenshot/photo attachment)
- Image sent as base64, uploaded to Gitea issue as attachment

Web:
- Feedback button in sidebar rail
- Modal with text area + send
- Auto-labels same as iOS

Gateway:
- Multipart image upload to Gitea issue assets API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 13:31:20 -05:00
parent 557bd80174
commit 96fa49fae2
4 changed files with 198 additions and 4 deletions

View File

@@ -56,6 +56,28 @@
let mobileNavOpen = $state(false);
let uploadInput: HTMLInputElement;
let feedbackOpen = $state(false);
let feedbackText = $state('');
let feedbackSending = $state(false);
let feedbackSent = $state(false);
async function sendFeedback() {
if (!feedbackText.trim()) return;
feedbackSending = true;
try {
const res = await fetch('/api/feedback', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: feedbackText.trim(), source: 'web' }),
});
if (res.ok) {
feedbackSent = true;
setTimeout(() => { feedbackOpen = false; feedbackSent = false; feedbackText = ''; }, 1500);
}
} catch {}
feedbackSending = false;
}
let uploadStatus = $state<'' | 'uploading' | 'done'>('');
async function handleUpload(file: File) {
@@ -176,6 +198,10 @@
</button>
</div>
<input bind:this={uploadInput} type="file" accept="image/*,.pdf" onchange={onUploadInput} hidden />
<button class="rail-feedback-btn" onclick={() => feedbackOpen = true} title="Send feedback">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="13" height="13"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
Feedback
</button>
<div class="rail-date">
<CalendarDays size={14} strokeWidth={1.8} />
<span>{new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric' }).format(new Date())}</span>
@@ -246,6 +272,28 @@
</div>
</div>
{#if feedbackOpen}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="feedback-overlay" onclick={() => feedbackOpen = false}>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="feedback-modal" onclick={(e) => e.stopPropagation()}>
{#if feedbackSent}
<div class="feedback-success">
<svg viewBox="0 0 24 24" fill="none" stroke="#059669" stroke-width="2.5" width="40" height="40"><path d="M20 6L9 17l-5-5"/></svg>
<div>Sent!</div>
</div>
{:else}
<div class="feedback-header">Feedback</div>
<div class="feedback-sub">Bug report, feature request, or anything</div>
<textarea class="feedback-input" bind:value={feedbackText} placeholder="What's on your mind?"></textarea>
<button class="feedback-send" onclick={sendFeedback} disabled={!feedbackText.trim() || feedbackSending}>
{feedbackSending ? 'Sending...' : 'Send'}
</button>
{/if}
</div>
</div>
{/if}
<style>
:global(body) {
background:
@@ -633,4 +681,63 @@
border-top: 1px solid var(--shell-line);
}
}
.rail-feedback-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 8px;
border: 1px dashed rgba(35,26,17,0.12);
background: none;
color: #8c7b69;
font-size: 0.72rem;
font-family: var(--font);
cursor: pointer;
transition: all 160ms;
}
.rail-feedback-btn:hover { background: rgba(255,248,241,0.8); color: #1e1812; }
.feedback-overlay {
position: fixed; inset: 0; z-index: 90;
background: rgba(18,12,8,0.34); backdrop-filter: blur(8px);
display: flex; align-items: center; justify-content: center;
}
.feedback-modal {
width: min(420px, calc(100vw - 32px));
padding: 24px;
border-radius: 22px;
background: linear-gradient(180deg, rgba(255,250,242,0.98), rgba(247,239,229,0.98));
box-shadow: 0 24px 60px rgba(35,24,15,0.2);
}
.feedback-header {
font-size: 1.1rem; font-weight: 700; color: #1e1812; margin-bottom: 4px;
}
.feedback-sub {
font-size: 0.78rem; color: #8c7b69; margin-bottom: 14px;
}
.feedback-input {
width: 100%; min-height: 100px; padding: 12px;
border-radius: 12px; border: 1px solid rgba(35,26,17,0.12);
background: rgba(255,255,255,0.6); color: #1e1812;
font-size: 0.92rem; font-family: var(--font); resize: vertical;
outline: none; margin-bottom: 12px;
}
.feedback-input:focus { border-color: rgba(35,26,17,0.3); }
.feedback-send {
width: 100%; padding: 12px;
border-radius: 12px; border: none;
background: #5d4737; color: white;
font-size: 0.92rem; font-weight: 600;
font-family: var(--font); cursor: pointer;
transition: opacity 160ms;
}
.feedback-send:hover { opacity: 0.9; }
.feedback-send:disabled { opacity: 0.4; cursor: not-allowed; }
.feedback-success {
display: flex; flex-direction: column;
align-items: center; gap: 10px;
padding: 32px; font-size: 1.1rem;
font-weight: 600; color: #059669;
}
</style>

View File

@@ -84,12 +84,16 @@
.dock-head,
.dock-card {
pointer-events: auto;
backdrop-filter: blur(16px);
border: 1px solid rgba(35, 26, 17, 0.1);
box-shadow: 0 24px 40px rgba(35, 24, 15, 0.14);
}
.dock-head,
.dock-card {
pointer-events: none;
}
.dock-head {
display: flex;
align-items: center;
@@ -209,6 +213,7 @@
}
.dock-action {
pointer-events: auto;
justify-self: start;
padding: 8px 12px;
border-radius: 999px;