feat: feedback with photo support + web dashboard feedback button
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:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user