Full rental marketplace with 6 categories (apartment, house, car, motorcycle, bicycle, ebike). Booking workflow: create → confirm → pay → active → complete → payout. Landlord dashboard, admin moderation, availability calendar, Stripe Connect payouts. 14 QA bugs found and fixed including validator schemas, API response types, HTTP methods. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
import { useState } from 'react';
|
|
import { Modal } from './ui/Modal';
|
|
import { Button } from './ui/Button';
|
|
import { GradientButton } from './ui/GradientButton';
|
|
import { api } from '../api/client';
|
|
import type { ReportReason } from '../types';
|
|
|
|
interface ReportModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
targetType: 'LISTING' | 'USER';
|
|
targetId: string;
|
|
}
|
|
|
|
const REASONS: { value: ReportReason; label: string }[] = [
|
|
{ value: 'SPAM', label: 'Spam' },
|
|
{ value: 'INAPPROPRIATE', label: 'Inappropriate content' },
|
|
{ value: 'SCAM', label: 'Scam / Fraud' },
|
|
{ value: 'COUNTERFEIT', label: 'Counterfeit item' },
|
|
{ value: 'PROHIBITED_ITEM', label: 'Prohibited item' },
|
|
{ value: 'HARASSMENT', label: 'Harassment' },
|
|
{ value: 'OTHER', label: 'Other' },
|
|
];
|
|
|
|
export function ReportModal({ isOpen, onClose, targetType, targetId }: ReportModalProps) {
|
|
const [reason, setReason] = useState<ReportReason | ''>('');
|
|
const [description, setDescription] = useState('');
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [success, setSuccess] = useState(false);
|
|
const [error, setError] = useState('');
|
|
|
|
const handleSubmit = async () => {
|
|
if (!reason) return;
|
|
setSubmitting(true);
|
|
setError('');
|
|
try {
|
|
await api.post('/reports', { targetType, targetId, reason, description: description || undefined });
|
|
setSuccess(true);
|
|
setTimeout(() => {
|
|
onClose();
|
|
setSuccess(false);
|
|
setReason('');
|
|
setDescription('');
|
|
}, 1500);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Failed to submit report');
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Modal isOpen={isOpen} onClose={onClose} title="Report" size="sm">
|
|
{success ? (
|
|
<div className="py-8 text-center">
|
|
<p className="text-green-600 font-medium">Report submitted. Thank you!</p>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{error && <p className="text-sm text-red-500 mb-3">{error}</p>}
|
|
<p className="text-sm text-gray-500 mb-4">Why are you reporting this?</p>
|
|
<div className="space-y-2 mb-4">
|
|
{REASONS.map((r) => (
|
|
<label key={r.value} className="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="reason"
|
|
value={r.value}
|
|
checked={reason === r.value}
|
|
onChange={() => setReason(r.value)}
|
|
className="text-primary-600"
|
|
/>
|
|
<span className="text-sm">{r.label}</span>
|
|
</label>
|
|
))}
|
|
</div>
|
|
<div className="mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1.5">Additional details (optional)</label>
|
|
<textarea
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
rows={3}
|
|
placeholder="Provide more details..."
|
|
className="w-full rounded-xl border border-gray-200 bg-white px-4 py-2.5 text-sm placeholder:text-gray-400 focus:border-primary-400 focus:ring-2 focus:ring-primary-100 focus:outline-none resize-none"
|
|
/>
|
|
</div>
|
|
<div className="flex gap-3">
|
|
<Button variant="secondary" className="flex-1" onClick={onClose}>Cancel</Button>
|
|
<GradientButton className="flex-1" onClick={handleSubmit} disabled={!reason || submitting}>
|
|
{submitting ? 'Submitting...' : 'Submit Report'}
|
|
</GradientButton>
|
|
</div>
|
|
</>
|
|
)}
|
|
</Modal>
|
|
);
|
|
}
|