Add an opt-in feedback submission flow that posts to the central feedback-api service. Integrated into the existing LogViewerCard so the same card now covers diagnostics capture (logs) and diagnostics forwarding (feedback). - Rust command `send_feedback` forwards the payload via reqwest, so the Tauri origin never needs a CORS whitelist entry server-side - First submission shows a one-time consent dialog explaining that this is the only app feature that talks to a server besides updates and Maximus sign-in - Three opt-in checkboxes (all unchecked by default): navigation context, recent error logs (appended as a suffix to the content), identify with the Maximus account - Context keys are limited to the server whitelist (page, locale, theme, viewport, userAgent, timestamp); app_version + OS are packed into userAgent via `get_feedback_user_agent` so we don't pull in an extra Tauri plugin - Error codes are stable strings (invalid, rate_limit, server_error, network_error) mapped to i18n messages on the frontend - Wording follows the cross-app convention documented in `la-compagnie-maximus/docs/feedback-hub-ops.md` - CSP `connect-src` extended with feedback.lacompagniemaximus.com - Docs: CHANGELOG (EN + FR), guide utilisateur, docs.settings (features/steps/tips) updated in both locales Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
67 lines
2.3 KiB
TypeScript
67 lines
2.3 KiB
TypeScript
import { useEffect } from "react";
|
|
import { createPortal } from "react-dom";
|
|
import { useTranslation } from "react-i18next";
|
|
import { X, Globe } from "lucide-react";
|
|
|
|
interface FeedbackConsentDialogProps {
|
|
onAccept: () => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export default function FeedbackConsentDialog({
|
|
onAccept,
|
|
onCancel,
|
|
}: FeedbackConsentDialogProps) {
|
|
const { t } = useTranslation();
|
|
|
|
useEffect(() => {
|
|
function handleEscape(e: KeyboardEvent) {
|
|
if (e.key === "Escape") onCancel();
|
|
}
|
|
document.addEventListener("keydown", handleEscape);
|
|
return () => document.removeEventListener("keydown", handleEscape);
|
|
}, [onCancel]);
|
|
|
|
return createPortal(
|
|
<div
|
|
className="fixed inset-0 z-[210] flex items-center justify-center bg-black/50"
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) onCancel();
|
|
}}
|
|
>
|
|
<div className="bg-[var(--card)] rounded-xl border border-[var(--border)] shadow-2xl w-full max-w-md mx-4">
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--border)]">
|
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
<Globe size={18} />
|
|
{t("feedback.consent.title")}
|
|
</h2>
|
|
<button
|
|
onClick={onCancel}
|
|
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)]"
|
|
aria-label={t("feedback.dialog.cancel")}
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
<div className="px-6 py-4 space-y-3 text-sm text-[var(--muted-foreground)]">
|
|
<p>{t("feedback.consent.body")}</p>
|
|
</div>
|
|
<div className="flex justify-end gap-2 px-6 py-4 border-t border-[var(--border)]">
|
|
<button
|
|
onClick={onCancel}
|
|
className="px-4 py-2 text-sm border border-[var(--border)] rounded-lg hover:bg-[var(--border)] transition-colors"
|
|
>
|
|
{t("feedback.dialog.cancel")}
|
|
</button>
|
|
<button
|
|
onClick={onAccept}
|
|
className="px-4 py-2 text-sm bg-[var(--primary)] text-[var(--primary-foreground)] rounded-lg hover:opacity-90 transition-opacity"
|
|
>
|
|
{t("feedback.consent.accept")}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body,
|
|
);
|
|
}
|