From 165039b4385bfe34cbf70501dba13a27e5f23235 Mon Sep 17 00:00:00 2001 From: Jaydon Bot Date: Mon, 2 Mar 2026 21:50:35 +0000 Subject: [PATCH 1/2] feat: add production-ready report modal flow for listing detail --- frontend/app/listings/[id].tsx | 29 +++++ frontend/components/ReportModal.tsx | 191 ++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 frontend/components/ReportModal.tsx diff --git a/frontend/app/listings/[id].tsx b/frontend/app/listings/[id].tsx index 7d1c42c..fb4e12a 100644 --- a/frontend/app/listings/[id].tsx +++ b/frontend/app/listings/[id].tsx @@ -1,10 +1,12 @@ import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Share, Alert } from 'react-native'; +import { useState } from 'react'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useQuery } from 'convex/react'; import { api } from 'convex/_generated/api'; import { Id } from 'convex/_generated/dataModel'; import ListingUnavailable from '../../components/ListingUnavailable'; import HiddenBanner from '../../components/HiddenBanner'; +import { ReportModal } from '../../components/ReportModal'; export default function ListingDetailScreen() { const { id } = useLocalSearchParams(); @@ -13,6 +15,7 @@ export default function ListingDetailScreen() { id: id as Id<'listings'>, }); const currentUserSubject = useQuery(api.listings.getCurrentUserSubject); + const [reportOpen, setReportOpen] = useState(false); const navigateToFeedWithTag = (tag: string) => { router.push({ @@ -93,6 +96,21 @@ export default function ListingDetailScreen() { )} + + {!isOwner && ( + + setReportOpen(true)}> + Report listing + + + )} + + setReportOpen(false)} + targetId={String(listing._id)} + targetType="listing" + /> ); } @@ -171,4 +189,15 @@ const styles = StyleSheet.create({ fontSize: 16, fontWeight: '600', }, + reportButton: { + borderWidth: 1, + borderColor: '#c62828', + padding: 12, + borderRadius: 8, + alignItems: 'center', + }, + reportButtonText: { + color: '#c62828', + fontWeight: '700', + }, }); diff --git a/frontend/components/ReportModal.tsx b/frontend/components/ReportModal.tsx new file mode 100644 index 0000000..1238a01 --- /dev/null +++ b/frontend/components/ReportModal.tsx @@ -0,0 +1,191 @@ +import { useState } from 'react'; +import { Alert, Modal, Pressable, StyleSheet, Text, TextInput, View } from 'react-native'; +import { useMutation } from 'convex/react'; +import { api } from 'convex/_generated/api'; + +type ReportReason = 'scam' | 'inappropriate' | 'spam'; + +type ReportModalProps = { + isVisible: boolean; + onClose: () => void; + targetId: string; + targetType: 'listing' | 'profile'; +}; + +const REASONS: { value: ReportReason; label: string; desc: string }[] = [ + { value: 'scam', label: 'Scam', desc: 'Fraudulent listing or deceptive behavior' }, + { value: 'inappropriate', label: 'Inappropriate', desc: 'Offensive or policy-violating content' }, + { value: 'spam', label: 'Spam', desc: 'Low-quality, duplicate, or irrelevant posting' }, +]; + +const MAX_NOTES = 500; + +export function ReportModal({ isVisible, onClose, targetId, targetType }: ReportModalProps) { + const createReport = useMutation(api.reports.createReport); + const [reason, setReason] = useState(null); + const [notes, setNotes] = useState(''); + const [submitting, setSubmitting] = useState(false); + + const reset = () => { + setReason(null); + setNotes(''); + setSubmitting(false); + }; + + const handleClose = () => { + reset(); + onClose(); + }; + + const handleSubmit = async () => { + if (!reason || submitting) return; + setSubmitting(true); + try { + await createReport({ + targetId, + targetType, + reason, + notes: notes.trim() ? notes.trim() : undefined, + }); + Alert.alert('Report submitted', 'Thanks for helping keep PolyBuys safe.'); + handleClose(); + } catch (err) { + const msg = String((err as Error)?.message ?? 'Unable to submit report right now.'); + Alert.alert('Could not submit report', msg); + setSubmitting(false); + } + }; + + return ( + + + {}}> + Report listing + Select a reason (required) + + {REASONS.map((r) => ( + setReason(r.value)} + accessibilityRole="radio" + accessibilityState={{ checked: reason === r.value }} + > + {r.label} + {r.desc} + + ))} + + Notes (optional) + + {notes.length}/{MAX_NOTES} + + + + Cancel + + + {submitting ? 'Submitting…' : 'Submit report'} + + + + + + ); +} + +const styles = StyleSheet.create({ + backdrop: { + flex: 1, + backgroundColor: 'rgba(0,0,0,0.45)', + justifyContent: 'center', + padding: 16, + }, + card: { + backgroundColor: '#fff', + borderRadius: 12, + padding: 16, + }, + title: { + fontSize: 20, + fontWeight: '700', + marginBottom: 8, + }, + subtitle: { + fontSize: 13, + color: '#444', + marginBottom: 8, + marginTop: 8, + }, + reasonRow: { + borderWidth: 1, + borderColor: '#d9d9d9', + borderRadius: 10, + padding: 10, + marginBottom: 8, + }, + reasonRowActive: { + borderColor: '#1976d2', + backgroundColor: '#eef5ff', + }, + reasonLabel: { + fontWeight: '600', + marginBottom: 2, + }, + reasonDesc: { + color: '#555', + fontSize: 12, + }, + notesInput: { + borderWidth: 1, + borderColor: '#d9d9d9', + borderRadius: 10, + minHeight: 80, + padding: 10, + textAlignVertical: 'top', + }, + counter: { + alignSelf: 'flex-end', + color: '#666', + fontSize: 12, + marginTop: 4, + }, + actions: { + marginTop: 14, + flexDirection: 'row', + justifyContent: 'flex-end', + gap: 8, + }, + cancelButton: { + paddingVertical: 10, + paddingHorizontal: 14, + }, + cancelText: { + color: '#333', + fontWeight: '600', + }, + submitButton: { + backgroundColor: '#c62828', + borderRadius: 8, + paddingVertical: 10, + paddingHorizontal: 14, + }, + submitButtonDisabled: { + opacity: 0.5, + }, + submitText: { + color: '#fff', + fontWeight: '700', + }, +}); From b4a7d9193ea2541b3a815225ea0b925420249bbd Mon Sep 17 00:00:00 2001 From: Jaydon Bot Date: Mon, 2 Mar 2026 21:52:52 +0000 Subject: [PATCH 2/2] style: format ReportModal with Prettier --- frontend/components/ReportModal.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/components/ReportModal.tsx b/frontend/components/ReportModal.tsx index 1238a01..22039a6 100644 --- a/frontend/components/ReportModal.tsx +++ b/frontend/components/ReportModal.tsx @@ -85,7 +85,9 @@ export function ReportModal({ isVisible, onClose, targetId, targetType }: Report onChangeText={setNotes} placeholder="Add details that help moderators review this report" /> - {notes.length}/{MAX_NOTES} + + {notes.length}/{MAX_NOTES} +