161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import Link from "next/link";
|
|
import { api } from "@/lib/api";
|
|
import { useAuth } from "@/lib/auth";
|
|
import { useToast } from "@/lib/toast";
|
|
import { useI18n } from "@/lib/i18n";
|
|
import { clearDraft, loadDraft, saveDraft } from "@/lib/localDraft";
|
|
|
|
const DRAFT_KEY = "nomadro-feedback-draft";
|
|
|
|
export default function FeedbackWidget() {
|
|
const { token } = useAuth();
|
|
const { toast } = useToast();
|
|
const { t } = useI18n();
|
|
const [open, setOpen] = useState(false);
|
|
const [type, setType] = useState("feature");
|
|
const [msg, setMsg] = useState("");
|
|
const [sending, setSending] = useState(false);
|
|
const [ready, setReady] = useState(false);
|
|
|
|
const types = [
|
|
{ key: "feature", label: `💡 ${t.feedback.catFeature}` },
|
|
{ key: "bug", label: `🐛 ${t.feedback.catBug}` },
|
|
{ key: "general", label: `❤️ ${t.feedback.catGeneral}` },
|
|
];
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const onKey = (e: KeyboardEvent) => {
|
|
if (e.key === "Escape") setOpen(false);
|
|
};
|
|
window.addEventListener("keydown", onKey);
|
|
return () => window.removeEventListener("keydown", onKey);
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
const draft = loadDraft<{ content?: string; category?: string }>(DRAFT_KEY);
|
|
if (draft) {
|
|
if (draft.content) setMsg(draft.content);
|
|
if (draft.category) setType(draft.category);
|
|
}
|
|
setReady(true);
|
|
}, [open]);
|
|
|
|
useEffect(() => {
|
|
if (!open || !ready) return;
|
|
const id = window.setTimeout(() => {
|
|
if (!msg.trim()) {
|
|
clearDraft(DRAFT_KEY);
|
|
return;
|
|
}
|
|
saveDraft(DRAFT_KEY, { content: msg, category: type });
|
|
}, 400);
|
|
return () => window.clearTimeout(id);
|
|
}, [msg, type, open, ready]);
|
|
|
|
const submit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (!msg.trim() || msg.trim().length < 5) {
|
|
toast(t.feedback.minLength, "info");
|
|
return;
|
|
}
|
|
setSending(true);
|
|
try {
|
|
await api.submitFeedback(msg.trim(), token || undefined, type);
|
|
try {
|
|
const key = "nomadro-feedback";
|
|
const prev = JSON.parse(localStorage.getItem(key) || "[]");
|
|
prev.push({ type, msg: msg.trim(), at: new Date().toISOString() });
|
|
localStorage.setItem(key, JSON.stringify(prev.slice(-50)));
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
clearDraft(DRAFT_KEY);
|
|
toast(t.feedback.ok, "success", {
|
|
href: "/community",
|
|
label: t.feedback.goCommunity,
|
|
});
|
|
setMsg("");
|
|
setOpen(false);
|
|
} catch {
|
|
toast(t.feedback.failRetry, "error");
|
|
} finally {
|
|
setSending(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
className="feedback-fab"
|
|
onClick={() => setOpen(true)}
|
|
aria-label={t.feedback.fabAria}
|
|
title={t.feedback.fabTitle}
|
|
>
|
|
💬
|
|
</button>
|
|
|
|
{open && (
|
|
<div
|
|
className="modal-overlay open"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={t.feedback.widgetTitle}
|
|
onClick={(e) => e.target === e.currentTarget && setOpen(false)}
|
|
>
|
|
<div className="modal feedback-modal">
|
|
<button className="modal-close" onClick={() => setOpen(false)} aria-label={t.feedback.close}>
|
|
✕
|
|
</button>
|
|
<div className="feedback-body">
|
|
<span className="section-tag">{t.feedback.tag}</span>
|
|
<h2>{t.feedback.widgetTitle}</h2>
|
|
<p>{t.feedback.widgetSubtitle}</p>
|
|
<form onSubmit={(e) => void submit(e)}>
|
|
<div className="feedback-types">
|
|
{types.map((item) => (
|
|
<button
|
|
key={item.key}
|
|
type="button"
|
|
className={`filter-btn${type === item.key ? " active" : ""}`}
|
|
onClick={() => setType(item.key)}
|
|
>
|
|
{item.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<textarea
|
|
className="feedback-textarea"
|
|
rows={4}
|
|
placeholder={t.feedback.placeholder}
|
|
value={msg}
|
|
onChange={(e) => setMsg(e.target.value)}
|
|
required
|
|
maxLength={500}
|
|
/>
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary"
|
|
disabled={sending || !msg.trim()}
|
|
style={{ width: "100%" }}
|
|
>
|
|
{sending ? t.feedback.submitting : t.feedback.sendBtn}
|
|
</button>
|
|
<p className="feedback-more-link">
|
|
<Link href="/feedback" onClick={() => setOpen(false)}>
|
|
{t.feedback.openFull}
|
|
</Link>
|
|
</p>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
);
|
|
}
|