feat: add UserFeedbackModal component and supporting unit tests
This commit is contained in:
@@ -7,6 +7,10 @@ import {
|
|||||||
getAnalyticsClientId,
|
getAnalyticsClientId,
|
||||||
getAnalyticsSessionId,
|
getAnalyticsSessionId,
|
||||||
} from "@/lib/app-analytics";
|
} from "@/lib/app-analytics";
|
||||||
|
import {
|
||||||
|
getSupabaseBrowserClient,
|
||||||
|
hasSupabasePublicEnv,
|
||||||
|
} from "@/lib/supabase/client";
|
||||||
import type { UserFeedbackEntry } from "@/types/ops";
|
import type { UserFeedbackEntry } from "@/types/ops";
|
||||||
|
|
||||||
export type FeedbackCategory = "bug" | "data" | "idea" | "payment" | "account" | "other";
|
export type FeedbackCategory = "bug" | "data" | "idea" | "payment" | "account" | "other";
|
||||||
@@ -60,6 +64,7 @@ export function UserFeedbackModal({
|
|||||||
const [category, setCategory] = useState<FeedbackCategory>("bug");
|
const [category, setCategory] = useState<FeedbackCategory>("bug");
|
||||||
const [message, setMessage] = useState("");
|
const [message, setMessage] = useState("");
|
||||||
const [contact, setContact] = useState("");
|
const [contact, setContact] = useState("");
|
||||||
|
const [loginEmailContact, setLoginEmailContact] = useState("");
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [submitted, setSubmitted] = useState(false);
|
const [submitted, setSubmitted] = useState(false);
|
||||||
@@ -68,10 +73,36 @@ export function UserFeedbackModal({
|
|||||||
if (!open) return;
|
if (!open) return;
|
||||||
setCategory(draft?.category || "bug");
|
setCategory(draft?.category || "bug");
|
||||||
setMessage("");
|
setMessage("");
|
||||||
|
setContact("");
|
||||||
|
setLoginEmailContact("");
|
||||||
setError("");
|
setError("");
|
||||||
setSubmitted(false);
|
setSubmitted(false);
|
||||||
}, [open, draft?.category]);
|
}, [open, draft?.category]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open || !hasSupabasePublicEnv()) return;
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
const loadLoginEmail = async () => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
data: { session },
|
||||||
|
} = await getSupabaseBrowserClient().auth.getSession();
|
||||||
|
const email = String(session?.user?.email || "").trim();
|
||||||
|
if (cancelled || !email) return;
|
||||||
|
setLoginEmailContact(email);
|
||||||
|
setContact(email);
|
||||||
|
} catch {
|
||||||
|
// Feedback can still be submitted; the backend will attach auth email when present.
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
void loadLoginEmail();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
const contextPreview = useMemo(() => {
|
const contextPreview = useMemo(() => {
|
||||||
const context = draft?.context || {};
|
const context = draft?.context || {};
|
||||||
const city = String(context.city || context.display_city || "").trim();
|
const city = String(context.city || context.display_city || "").trim();
|
||||||
@@ -197,12 +228,20 @@ export function UserFeedbackModal({
|
|||||||
|
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="text-xs font-bold text-slate-600">
|
<span className="text-xs font-bold text-slate-600">
|
||||||
{isEn ? "Contact, optional" : "联系方式,可选"}
|
{loginEmailContact
|
||||||
|
? isEn ? "Contact (login email)" : "联系方式(登录邮箱)"
|
||||||
|
: isEn ? "Contact, optional" : "联系方式,可选"}
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
value={contact}
|
value={contact}
|
||||||
onChange={(event) => setContact(event.target.value)}
|
onChange={(event) => {
|
||||||
className="mt-1 h-9 w-full rounded border border-slate-200 bg-white px-3 text-sm text-slate-900 outline-none transition focus:border-blue-400 focus:ring-2 focus:ring-blue-100"
|
if (!loginEmailContact) setContact(event.target.value);
|
||||||
|
}}
|
||||||
|
readOnly={Boolean(loginEmailContact)}
|
||||||
|
className={clsx(
|
||||||
|
"mt-1 h-9 w-full rounded border border-slate-200 px-3 text-sm text-slate-900 outline-none transition focus:border-blue-400 focus:ring-2 focus:ring-blue-100",
|
||||||
|
loginEmailContact ? "bg-slate-50 text-slate-600" : "bg-white",
|
||||||
|
)}
|
||||||
placeholder={isEn ? "Email or Telegram" : "邮箱或 Telegram"}
|
placeholder={isEn ? "Email or Telegram" : "邮箱或 Telegram"}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -48,10 +48,14 @@ export function runTests() {
|
|||||||
assert(
|
assert(
|
||||||
modalSource.includes("/api/feedback") &&
|
modalSource.includes("/api/feedback") &&
|
||||||
modalSource.includes("getAnalyticsClientId") &&
|
modalSource.includes("getAnalyticsClientId") &&
|
||||||
|
modalSource.includes("getSupabaseBrowserClient") &&
|
||||||
|
modalSource.includes("hasSupabasePublicEnv") &&
|
||||||
|
modalSource.includes(".auth.getSession()") &&
|
||||||
|
modalSource.includes("readOnly={Boolean(loginEmailContact)}") &&
|
||||||
modalSource.includes("onSubmitted") &&
|
modalSource.includes("onSubmitted") &&
|
||||||
modalSource.includes("navigator.userAgent") &&
|
modalSource.includes("navigator.userAgent") &&
|
||||||
modalSource.includes("type=\"textarea\"") === false,
|
modalSource.includes("type=\"textarea\"") === false,
|
||||||
"feedback modal must submit to the feedback API, notify the dashboard after success, and attach client/session diagnostics without using invalid textarea input types",
|
"feedback modal must submit to the feedback API, lock contact to the login email when available, notify the dashboard after success, and attach client/session diagnostics without using invalid textarea input types",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
feedbackRouteSource.includes("export async function GET") &&
|
feedbackRouteSource.includes("export async function GET") &&
|
||||||
|
|||||||
Reference in New Issue
Block a user