14d7127e41
- Error Feedback Service: POST /api/feedback saves to JSONL, GET with admin token - ErrorFeedback component: warm amber tones, friendly messages, 'Report Issue' button - Replaced ALL red-themed error displays across 15+ pages with ErrorFeedback - Kind classification: network, auth, location, upgrade, default messages - QA test suite v2: 8-layer testing (system, API, render, scenario, edge, mobile, perf, security) - Browser-based visual QA with Playwright (Layer 9)
176 lines
6.2 KiB
TypeScript
176 lines
6.2 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useCallback } from "react";
|
|
import { AlertTriangle, Send, X, Bug } from "lucide-react";
|
|
|
|
// ── Feedback submission API ──
|
|
async function submitFeedback(data: {
|
|
url: string;
|
|
error: string;
|
|
message?: string;
|
|
userAgent: string;
|
|
}) {
|
|
try {
|
|
const res = await fetch("/mobile/api/feedback", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(data),
|
|
});
|
|
return res.ok;
|
|
} catch {
|
|
return false; // silently fail — we don't want a feedback-on-feedback loop
|
|
}
|
|
}
|
|
|
|
// ── Friendly error messages map ──
|
|
const FRIENDLY_MESSAGES: Record<string, { title: string; description: string }> = {
|
|
default: {
|
|
title: "A small glitch 🛠️",
|
|
description:
|
|
"Something didn't load quite right. Don't worry — we're on it! Try again or drop us a report so we can fix it faster.",
|
|
},
|
|
location: {
|
|
title: "Location not available 📡",
|
|
description:
|
|
"We couldn't detect your location. Make sure location services are enabled, or try refreshing. The compass will work even without location!",
|
|
},
|
|
network: {
|
|
title: "Connection hiccup 🌐",
|
|
description:
|
|
"Looks like the signal dropped for a moment. Check your connection and try again. If it persists, let us know!",
|
|
},
|
|
auth: {
|
|
title: "Session expired 🔑",
|
|
description:
|
|
"Your session ran out — it happens! Just sign in again and you'll be right back where you were.",
|
|
},
|
|
upgrade: {
|
|
title: "Premium feature ✨",
|
|
description:
|
|
"This feature needs an upgrade. Check your plan details to unlock it!",
|
|
},
|
|
};
|
|
|
|
type ErrorKind = keyof typeof FRIENDLY_MESSAGES;
|
|
|
|
interface ErrorFeedbackProps {
|
|
/** The raw error message from the system */
|
|
error: string;
|
|
/** Category of error for friendly message mapping */
|
|
kind?: ErrorKind;
|
|
/** Callback to retry the action */
|
|
onRetry?: () => void;
|
|
/** Additional context for debugging */
|
|
context?: string;
|
|
}
|
|
|
|
export default function ErrorFeedback({
|
|
error,
|
|
kind = "default",
|
|
onRetry,
|
|
context,
|
|
}: ErrorFeedbackProps) {
|
|
const [showForm, setShowForm] = useState(false);
|
|
const [userMessage, setUserMessage] = useState("");
|
|
const [sent, setSent] = useState(false);
|
|
const [sending, setSending] = useState(false);
|
|
|
|
const friendly = FRIENDLY_MESSAGES[kind] || FRIENDLY_MESSAGES.default;
|
|
|
|
const handleSend = useCallback(async () => {
|
|
setSending(true);
|
|
const ok = await submitFeedback({
|
|
url: window.location.href,
|
|
error: error + (context ? ` [${context}]` : ""),
|
|
message: userMessage,
|
|
userAgent: navigator.userAgent,
|
|
});
|
|
setSent(true);
|
|
setSending(false);
|
|
if (ok) {
|
|
setTimeout(() => setShowForm(false), 2000);
|
|
}
|
|
}, [error, context, userMessage]);
|
|
|
|
return (
|
|
<div className="mx-4 mb-4">
|
|
<div className="bg-amber-900/10 border border-amber-700/30 rounded-2xl p-5">
|
|
{/* ── Friendly header ── */}
|
|
<div className="flex items-start gap-3">
|
|
<div className="w-10 h-10 rounded-xl bg-amber-900/20 flex items-center justify-center shrink-0 mt-0.5">
|
|
<AlertTriangle size={20} className="text-amber-400" />
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<h3 className="text-sm font-semibold text-amber-200 mb-1">
|
|
{friendly.title}
|
|
</h3>
|
|
<p className="text-xs text-amber-300/70 leading-relaxed">
|
|
{friendly.description}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Actions ── */}
|
|
<div className="flex flex-wrap gap-2 mt-4">
|
|
{onRetry && (
|
|
<button
|
|
onClick={onRetry}
|
|
className="flex-1 min-w-[80px] px-4 py-3 bg-amber-900/20 hover:bg-amber-900/30 border border-amber-700/30 text-amber-300 text-sm font-medium rounded-xl active:scale-95 transition min-h-[44px]"
|
|
>
|
|
Try Again
|
|
</button>
|
|
)}
|
|
|
|
<button
|
|
onClick={() => setShowForm((s) => !s)}
|
|
className="flex-1 min-w-[80px] px-4 py-3 bg-gray-800/60 hover:bg-gray-700/60 border border-gray-700/40 text-gray-300 text-sm rounded-xl active:scale-95 transition min-h-[44px] flex items-center justify-center gap-1.5"
|
|
>
|
|
<Bug size={14} />
|
|
Report Issue
|
|
</button>
|
|
</div>
|
|
|
|
{/* ── Feedback form (expandable) ── */}
|
|
{showForm && (
|
|
<div className="mt-4 pt-4 border-t border-amber-800/20">
|
|
{sent ? (
|
|
<p className="text-xs text-emerald-400 text-center">
|
|
✓ Thanks! Your report helps us improve.
|
|
</p>
|
|
) : (
|
|
<>
|
|
<p className="text-xs text-amber-300/60 mb-2">
|
|
What happened? (optional — helps us fix it faster)
|
|
</p>
|
|
<textarea
|
|
value={userMessage}
|
|
onChange={(e) => setUserMessage(e.target.value)}
|
|
placeholder="e.g. The page was blank, then this showed up..."
|
|
rows={2}
|
|
className="w-full bg-gray-900/60 border border-gray-700/40 rounded-xl px-3 py-2 text-xs text-gray-300 placeholder:text-gray-600 resize-none focus:outline-none focus:border-amber-700/50"
|
|
/>
|
|
<div className="flex gap-2 mt-2">
|
|
<button
|
|
onClick={handleSend}
|
|
disabled={sending}
|
|
className="flex-1 px-4 py-2.5 bg-amber-600/20 hover:bg-amber-600/30 border border-amber-600/30 text-amber-300 text-xs font-medium rounded-xl active:scale-95 transition min-h-[40px] disabled:opacity-50 flex items-center justify-center gap-1.5"
|
|
>
|
|
<Send size={12} />
|
|
{sending ? "Sending..." : "Send Report"}
|
|
</button>
|
|
<button
|
|
onClick={() => setShowForm(false)}
|
|
className="px-3 py-2.5 bg-gray-800/40 border border-gray-700/30 text-gray-500 text-xs rounded-xl active:scale-95 transition min-h-[40px]"
|
|
>
|
|
<X size={14} />
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|