69d17302e1
- Nur Formatierung und Importreihenfolge, keine Verhaltensaenderung - Betrifft ausschliesslich Befunde, die der Merge neu eingebracht hat Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
196 lines
7.0 KiB
TypeScript
196 lines
7.0 KiB
TypeScript
'use client';
|
|
|
|
import Link from 'next/link';
|
|
import { useTranslations } from 'next-intl';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import { appVersion } from '@/lib/app-version';
|
|
import { dataUrlToBlob, sendBugReport } from '@/lib/bug-report-api';
|
|
import { getDesktopClientInfo, isDesktopClient } from '@/lib/desktop-client';
|
|
import { formatErrorsForReport } from '@/lib/error-buffer';
|
|
|
|
/**
|
|
* Dialog des Fehler-melden-Knopfs (quick-260914-m97). Muster:
|
|
* `marketplace/components/ActivationDialog.tsx` (Overlay, `role="dialog"`,
|
|
* Escape, Fokus). Bekommt das bereits aufgenommene Bild als Data-URL —
|
|
* die Aufnahme passiert im Knopf, BEVOR dieser Dialog erscheint.
|
|
*
|
|
* Der Dialog wird nur eingehaengt, solange er offen ist (quick-260921-ldf):
|
|
* jedes Oeffnen ist ein frischer Mount, damit gilt der Anfangszustand schon
|
|
* im ERSTEN Commit. Vorher zog ein Effekt den Zustand erst einen Commit
|
|
* spaeter nach — dazwischen stand sichtbar "Bild da, Haekchen aus" im DOM.
|
|
*/
|
|
interface BugReportDialogProps {
|
|
screenshot: string | null;
|
|
isAdmin: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
type Status = 'ready' | 'sending' | 'sent' | 'failed';
|
|
|
|
export function BugReportDialog({ screenshot, isAdmin, onClose }: BugReportDialogProps) {
|
|
const t = useTranslations('bugReport');
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
const [status, setStatus] = useState<Status>('ready');
|
|
const [failedStatus, setFailedStatus] = useState(0);
|
|
const [description, setDescription] = useState('');
|
|
// Das Haekchen wird beim Rendern aus `screenshot` abgeleitet, nicht per
|
|
// Effekt nachgezogen: gibt es ein Bild, ist es an. `attachChoice` haelt
|
|
// allein die bewusste Abwahl des Nutzers.
|
|
const [attachChoice, setAttachChoice] = useState<boolean | null>(null);
|
|
const attach = screenshot !== null && (attachChoice ?? true);
|
|
|
|
useEffect(() => {
|
|
textareaRef.current?.focus();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const handler = (e: KeyboardEvent) => {
|
|
if (e.key === 'Escape' && status !== 'sending') onClose();
|
|
};
|
|
document.addEventListener('keydown', handler);
|
|
return () => document.removeEventListener('keydown', handler);
|
|
}, [status, onClose]);
|
|
|
|
const handleSend = async () => {
|
|
setStatus('sending');
|
|
// Herkunft (quick-260918-gza): tessera_desktop entscheidet Desktop/
|
|
// Browser, tessera_desktop_client liefert die Details dazu; fehlt es
|
|
// (alter Client), bleibt es bei Desktop ohne Details.
|
|
const desktop = isDesktopClient();
|
|
const info = desktop ? getDesktopClientInfo() : null;
|
|
const result = await sendBugReport({
|
|
description: description.trim(),
|
|
page: window.location.pathname + window.location.search,
|
|
webVersion: appVersion.version,
|
|
webChannel: appVersion.channel,
|
|
webCommit: appVersion.commit,
|
|
userAgent: navigator.userAgent,
|
|
viewport: `${window.innerWidth}x${window.innerHeight}`,
|
|
clientTime: new Date().toISOString(),
|
|
errors: formatErrorsForReport(),
|
|
screenshot: attach && screenshot ? dataUrlToBlob(screenshot) : null,
|
|
clientKind: desktop ? 'desktop' : 'browser',
|
|
clientOs: info?.os ?? '',
|
|
clientVersion: info?.version ?? '',
|
|
clientCommit: info?.commit ?? '',
|
|
});
|
|
if (result.ok) {
|
|
setStatus('sent');
|
|
} else {
|
|
setFailedStatus(result.status);
|
|
setStatus('failed');
|
|
}
|
|
};
|
|
|
|
const errorKey =
|
|
failedStatus === 409
|
|
? 'errorNotConfigured'
|
|
: failedStatus === 429
|
|
? 'errorTooMany'
|
|
: failedStatus === 413
|
|
? 'errorTooLarge'
|
|
: failedStatus === 502
|
|
? 'errorSendFailed'
|
|
: 'errorGeneric';
|
|
|
|
const busy = status === 'sending';
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-labelledby="bug-report-title"
|
|
data-bug-report-ignore="true"
|
|
>
|
|
<div className="w-full max-w-lg rounded-lg bg-card dark:border dark:border-border p-6 shadow-lg">
|
|
<h2 id="bug-report-title" className="text-lg font-semibold text-foreground mb-2">
|
|
{t('title')}
|
|
</h2>
|
|
|
|
{status === 'sent' ? (
|
|
<>
|
|
<p className="text-sm text-foreground mb-4">{t('sent')}</p>
|
|
<div className="flex justify-end">
|
|
<button type="button" onClick={onClose} className="btn btn-primary">
|
|
{t('close')}
|
|
</button>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<p className="text-sm text-muted-foreground mb-4">{t('intro')}</p>
|
|
|
|
{screenshot ? (
|
|
<img
|
|
src={screenshot}
|
|
alt={t('screenshotAlt')}
|
|
className="max-h-48 w-auto rounded border border-border mb-3"
|
|
/>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground mb-3">{t('screenshotUnavailable')}</p>
|
|
)}
|
|
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<input
|
|
id="bug-report-attach"
|
|
type="checkbox"
|
|
className="h-4 w-4"
|
|
checked={attach}
|
|
disabled={screenshot === null || busy}
|
|
onChange={(e) => setAttachChoice(e.target.checked)}
|
|
/>
|
|
<label htmlFor="bug-report-attach" className="text-sm text-foreground">
|
|
{t('attachScreenshot')}
|
|
</label>
|
|
</div>
|
|
|
|
<label htmlFor="bug-report-description" className="mb-1 block text-sm text-foreground">
|
|
{t('descriptionLabel')}
|
|
</label>
|
|
<textarea
|
|
id="bug-report-description"
|
|
ref={textareaRef}
|
|
rows={4}
|
|
maxLength={4000}
|
|
placeholder={t('descriptionPlaceholder')}
|
|
className="w-full rounded border border-border bg-background px-3 py-2 text-sm text-foreground"
|
|
value={description}
|
|
disabled={busy}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
/>
|
|
|
|
{status === 'failed' && (
|
|
<div className="mt-3 text-sm text-destructive">
|
|
<p>{t(errorKey)}</p>
|
|
{failedStatus === 409 && isAdmin && (
|
|
<p className="mt-1">
|
|
{t('errorNotConfiguredAdminHint')}{' '}
|
|
<Link href="/admin/smtp" className="underline">
|
|
{t('errorNotConfiguredAdminLink')}
|
|
</Link>
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-4 flex justify-end gap-3">
|
|
<button type="button" onClick={onClose} disabled={busy} className="btn btn-secondary">
|
|
{t('cancel')}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSend}
|
|
disabled={busy}
|
|
className="btn btn-primary"
|
|
>
|
|
{busy ? t('sending') : t('send')}
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|