Spaces:
Sleeping
Sleeping
File size: 6,405 Bytes
f169a35 25cb9ab f169a35 25cb9ab f169a35 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 | import { useState } from "react";
import { apiPost } from "../lib/api";
const CATEGORIES: { value: string; label: string }[] = [
{ value: "bug", label: "Bug" },
{ value: "feature", label: "Feature request" },
{ value: "other", label: "Other" },
];
function FlagIcon() {
return (
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 21V4a1 1 0 0 1 1-1h11.5a.5.5 0 0 1 .4.8l-3 4a.5.5 0 0 0 0 .6l3 4a.5.5 0 0 1-.4.8H6" />
</svg>
);
}
export function ReportIssueButton({ className }: { className?: string }) {
const [open, setOpen] = useState(false);
return (
<>
<button
onClick={() => setOpen(true)}
className={className || "flex items-center gap-1.5 text-sm text-white/60 hover:text-white transition-colors"}
title="Report an issue"
>
<FlagIcon />
Report an issue
</button>
{open && <ReportIssueModal onClose={() => setOpen(false)} />}
</>
);
}
function ReportIssueModal({ onClose }: { onClose: () => void }) {
const [category, setCategory] = useState("bug");
const [subject, setSubject] = useState("");
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const [done, setDone] = useState(false);
const handleSubmit = async () => {
if (!subject.trim() || !description.trim()) {
setError("Please fill in both a subject and a description.");
return;
}
setSubmitting(true);
setError("");
try {
await apiPost("/api/issues", {
category,
subject: subject.trim(),
description: description.trim(),
page_context: window.location.hash || window.location.pathname,
});
setDone(true);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to submit — please try again.");
} finally {
setSubmitting(false);
}
};
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 p-4"
onClick={onClose}
>
<div
className="w-full max-w-md rounded-2xl bg-[var(--color-surface)] border border-[var(--color-border)] shadow-xl p-6 space-y-4"
onClick={(e) => e.stopPropagation()}
>
{done ? (
<div className="text-center py-4 space-y-2">
<div className="mx-auto flex items-center justify-center w-10 h-10 rounded-full bg-emerald-500/10 text-emerald-500">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12.5 9.5 18 20 6" /></svg>
</div>
<p className="text-sm font-medium text-[var(--color-text)]">Thanks — your report was sent.</p>
<button
onClick={onClose}
className="mt-2 text-sm px-4 py-1.5 rounded-lg bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-light)] transition-colors"
>
Close
</button>
</div>
) : (
<>
<div className="flex items-center justify-between">
<h3 className="text-sm font-bold uppercase tracking-widest text-[var(--color-text)]">Report an Issue</h3>
<button onClick={onClose} className="text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition-colors" title="Close">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M5 5l14 14M19 5 5 19" /></svg>
</button>
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Type</label>
<div className="flex gap-2">
{CATEGORIES.map((c) => (
<button
key={c.value}
onClick={() => setCategory(c.value)}
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
category === c.value
? "bg-[var(--color-primary)] text-white border-[var(--color-primary)]"
: "text-[var(--color-text-muted)] border-[var(--color-border)] hover:border-[var(--color-primary)]/50"
}`}
>
{c.label}
</button>
))}
</div>
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Subject</label>
<input
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Short summary of the issue"
className="input text-sm"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="What happened, what did you expect, and any steps to reproduce it?"
rows={4}
className="input text-sm resize-none"
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
<div className="flex justify-end gap-2 pt-1">
<button
onClick={onClose}
className="text-sm px-4 py-1.5 rounded-lg border border-[var(--color-border)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition-colors"
>
Cancel
</button>
<button
onClick={() => { void handleSubmit(); }}
disabled={submitting}
className="text-sm px-4 py-1.5 rounded-lg bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-light)] disabled:opacity-40 transition-colors"
>
{submitting ? "Sending…" : "Send report"}
</button>
</div>
</>
)}
</div>
</div>
);
}
|