220 lines
8.2 KiB
TypeScript
220 lines
8.2 KiB
TypeScript
'use client';
|
|
|
|
import React, { useState, useEffect, useRef } from 'react';
|
|
import { SupportTicketSummary } from './types';
|
|
|
|
interface SupportReportModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSuccess: (ticket: SupportTicketSummary) => void;
|
|
}
|
|
|
|
export function SupportReportModal({ isOpen, onClose, onSuccess }: SupportReportModalProps) {
|
|
const [ticketType, setTicketType] = useState('general');
|
|
const [reporterEmail, setReporterEmail] = useState('');
|
|
const [subject, setSubject] = useState('');
|
|
const [description, setDescription] = useState('');
|
|
const [severity, setSeverity] = useState('medium');
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const modalRef = useRef<HTMLDivElement>(null);
|
|
const firstInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
useEffect(() => {
|
|
if (isOpen) {
|
|
setTicketType('general');
|
|
setReporterEmail('');
|
|
setSubject('');
|
|
setDescription('');
|
|
setSeverity('medium');
|
|
setError(null);
|
|
|
|
// Focus first input after render
|
|
setTimeout(() => firstInputRef.current?.focus(), 100);
|
|
}
|
|
}, [isOpen]);
|
|
|
|
// Keyboard accessibility
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
if (!isOpen) return;
|
|
if (e.key === 'Escape') {
|
|
onClose();
|
|
}
|
|
};
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [isOpen, onClose]);
|
|
|
|
if (!isOpen) return null;
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (isSubmitting) return;
|
|
|
|
const trimmedEmail = reporterEmail.trim();
|
|
const trimmedSubject = subject.trim();
|
|
const trimmedDescription = description.trim();
|
|
|
|
if (!trimmedSubject) {
|
|
setError('Subject cannot be empty or contain only whitespace.');
|
|
return;
|
|
}
|
|
if (!trimmedDescription) {
|
|
setError('Description cannot be empty or contain only whitespace.');
|
|
return;
|
|
}
|
|
|
|
setIsSubmitting(true);
|
|
setError(null);
|
|
try {
|
|
const resp = await fetch('/api/ops/support/tickets', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
ticket_type: ticketType,
|
|
reporter_email: trimmedEmail,
|
|
subject: trimmedSubject,
|
|
description: trimmedDescription,
|
|
severity,
|
|
})
|
|
});
|
|
|
|
const data = await resp.json().catch(() => null);
|
|
if (resp.ok) {
|
|
onSuccess(data);
|
|
onClose();
|
|
} else {
|
|
setError(data?.detail || 'Failed to submit report');
|
|
}
|
|
} catch {
|
|
setError('Network error during submission');
|
|
} finally {
|
|
setIsSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-in fade-in duration-200" role="dialog" aria-modal="true" aria-labelledby="modal-title">
|
|
<div
|
|
ref={modalRef}
|
|
className="bg-surface-container-high border border-outline-variant/30 w-full max-w-lg rounded-2xl shadow-xl overflow-hidden flex flex-col"
|
|
>
|
|
<div className="px-6 py-4 border-b border-outline-variant/20 flex items-center justify-between">
|
|
<h2 id="modal-title" className="text-lg font-bold text-on-surface">New Support Report</h2>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="p-1 rounded-full text-outline hover:bg-surface-container-highest hover:text-on-surface transition-colors"
|
|
aria-label="Close"
|
|
>
|
|
<span className="material-symbols-outlined text-[20px]">close</span>
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-6 space-y-4">
|
|
{error && (
|
|
<div className="p-3 text-sm bg-error/10 text-error border border-error/20 rounded-lg">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="reporter_email" className="block text-sm font-medium text-on-surface">Reporter Email</label>
|
|
<input
|
|
id="reporter_email"
|
|
ref={firstInputRef}
|
|
type="email"
|
|
required
|
|
minLength={3}
|
|
maxLength={255}
|
|
value={reporterEmail}
|
|
onChange={e => setReporterEmail(e.target.value)}
|
|
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50"
|
|
placeholder="user@example.com"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="ticket_type" className="block text-sm font-medium text-on-surface">Ticket Type</label>
|
|
<select
|
|
id="ticket_type"
|
|
value={ticketType}
|
|
onChange={e => setTicketType(e.target.value)}
|
|
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50"
|
|
>
|
|
<option value="general">General</option>
|
|
<option value="bug_report">Bug Report</option>
|
|
<option value="telemetry_issue">Telemetry Issue</option>
|
|
<option value="contact_request">Contact Request</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="severity" className="block text-sm font-medium text-on-surface">Severity</label>
|
|
<select
|
|
id="severity"
|
|
value={severity}
|
|
onChange={e => setSeverity(e.target.value)}
|
|
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50"
|
|
>
|
|
<option value="low">Low</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="high">High</option>
|
|
<option value="critical">Critical</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="subject" className="block text-sm font-medium text-on-surface">Subject</label>
|
|
<input
|
|
id="subject"
|
|
type="text"
|
|
required
|
|
minLength={1}
|
|
maxLength={255}
|
|
value={subject}
|
|
onChange={e => setSubject(e.target.value)}
|
|
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50"
|
|
placeholder="Brief summary"
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label htmlFor="description" className="block text-sm font-medium text-on-surface">Description</label>
|
|
<textarea
|
|
id="description"
|
|
required
|
|
minLength={1}
|
|
maxLength={10000}
|
|
value={description}
|
|
onChange={e => setDescription(e.target.value)}
|
|
className="w-full px-3 py-2 bg-surface-container-lowest border border-outline-variant/50 rounded-lg text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/50 min-h-[120px]"
|
|
placeholder="Detailed description..."
|
|
/>
|
|
</div>
|
|
|
|
<div className="pt-4 flex items-center justify-end gap-3 border-t border-outline-variant/20">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={isSubmitting}
|
|
className="px-4 py-2 text-sm font-semibold text-on-surface-variant hover:text-on-surface transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="px-5 py-2 text-sm font-semibold bg-primary text-on-primary rounded-xl hover:bg-primary/90 transition-colors shadow-sm disabled:opacity-50"
|
|
>
|
|
{isSubmitting ? 'Submitting...' : 'Create Ticket'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |