Files
marketplace/client/src/components/ReportModal.tsx
delta-lynx-89e8 dbbbbd26f4 Add rental system: listings, bookings, payments, payouts, reviews
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>
2026-02-22 15:33:29 -08:00

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>
);
}