Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 52 additions & 49 deletions frontend/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { StatusBar } from 'expo-status-bar';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { Platform, useColorScheme } from 'react-native';
import { FlashProvider } from '../contexts/FlashContext';
import { usePushNotifications } from '../hooks/usePushNotifications';

const allowSentryPii = process.env.EXPO_PUBLIC_ENABLE_SENTRY_PII === 'true';
Expand Down Expand Up @@ -51,56 +52,58 @@ function RootLayout() {
<SafeAreaProvider>
<ConvexAuthProvider client={convex} storage={storage}>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<PushNotificationsBootstrap />
<StatusBar style="auto" />
<Stack
screenOptions={{
headerBackButtonDisplayMode: 'minimal',
}}
>
<Stack.Screen name="(tabs)" options={{ headerShown: false, title: 'Home' }} />
<Stack.Screen
name="listings/[id]"
options={{ title: 'Listing Details', headerBackTitle: 'Back' }}
/>
<Stack.Screen
name="listings/new"
options={{ title: 'Create Listing', headerBackTitle: 'Back' }}
/>
<Stack.Screen
name="listings/[id]/edit"
options={{ title: 'Edit Listing', headerBackTitle: 'Back' }}
/>
<Stack.Screen
name="messages/[id]"
options={{ title: 'Messages', headerBackTitle: 'Back' }}
/>
<Stack.Screen
name="conversations/[id]"
options={{ title: 'Conversation', headerBackTitle: 'Inbox' }}
/>
<Stack.Screen
name="auth/login"
options={{
title: 'Sign In',
headerBackTitle: 'Back',
headerShown: Platform.OS !== 'web',
<FlashProvider>
<PushNotificationsBootstrap />
<StatusBar style="auto" />
<Stack
screenOptions={{
headerBackButtonDisplayMode: 'minimal',
}}
/>
<Stack.Screen
name="account-settings"
options={{ title: 'Settings', headerBackTitle: 'Profile' }}
/>
<Stack.Screen
name="profile/edit"
options={{ title: 'Edit Profile', headerBackTitle: 'Profile' }}
/>
<Stack.Screen
name="profile/[userId]"
options={{ title: 'Profile', headerBackTitle: 'Back' }}
/>
<Stack.Screen name="l/[id]" options={{ headerShown: false }} />
</Stack>
>
<Stack.Screen name="(tabs)" options={{ headerShown: false, title: 'Home' }} />
<Stack.Screen
name="listings/[id]"
options={{ title: 'Listing Details', headerBackTitle: 'Back' }}
/>
<Stack.Screen
name="listings/new"
options={{ title: 'Create Listing', headerBackTitle: 'Back' }}
/>
<Stack.Screen
name="listings/[id]/edit"
options={{ title: 'Edit Listing', headerBackTitle: 'Back' }}
/>
<Stack.Screen
name="messages/[id]"
options={{ title: 'Messages', headerBackTitle: 'Back' }}
/>
<Stack.Screen
name="conversations/[id]"
options={{ title: 'Conversation', headerBackTitle: 'Inbox' }}
/>
<Stack.Screen
name="auth/login"
options={{
title: 'Sign In',
headerBackTitle: 'Back',
headerShown: Platform.OS !== 'web',
}}
/>
<Stack.Screen
name="account-settings"
options={{ title: 'Settings', headerBackTitle: 'Profile' }}
/>
<Stack.Screen
name="profile/edit"
options={{ title: 'Edit Profile', headerBackTitle: 'Profile' }}
/>
<Stack.Screen
name="profile/[userId]"
options={{ title: 'Profile', headerBackTitle: 'Back' }}
/>
<Stack.Screen name="l/[id]" options={{ headerShown: false }} />
</Stack>
</FlashProvider>
</ThemeProvider>
</ConvexAuthProvider>
</SafeAreaProvider>
Expand Down
5 changes: 5 additions & 0 deletions frontend/app/listings/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import Constants from 'expo-constants';
import { useMutation, useQuery } from 'convex/react';
import { api } from 'convex/_generated/api';
import { Id } from 'convex/_generated/dataModel';
import { useFlash } from '../../contexts/FlashContext';
import { useAuth } from '../../hooks/useAuth';
import HiddenBanner from '../../components/HiddenBanner';
import ListingUnavailable from '../../components/ListingUnavailable';
Expand All @@ -36,6 +37,7 @@ import { Chip } from '../../components/ui';
import ImageLightbox from '../../components/ImageLightbox';

import { APP_STORE_URL } from '../../constants/app';
import { REPORT_SUBMITTED_MESSAGE } from '../../constants/feedbackMessages';

type FeedTabHref = '/' | '/search' | '/settings';
const DEFAULT_APP_ORIGIN = 'https://polybuys.com';
Expand Down Expand Up @@ -68,6 +70,7 @@ export default function ListingDetailScreen() {
const { id } = useLocalSearchParams<{ id?: string | string[] }>();
const listingId = typeof id === 'string' && id.trim().length > 0 ? id : null;
const router = useRouter();
const { setFlash } = useFlash();
const { isAuthenticated } = useAuth();
const entranceStyle = useEntranceAnimation();
const appOrigin = getAppOrigin();
Expand Down Expand Up @@ -194,6 +197,7 @@ export default function ListingDetailScreen() {
try {
setMarkingSold(true);
await updateListingStatus({ id: listing._id, status: 'sold' });
setFlash('Listing marked as sold.');
} catch (error) {
const message =
error instanceof Error && error.message
Expand Down Expand Up @@ -561,6 +565,7 @@ export default function ListingDetailScreen() {
onClose={() => setReportOpen(false)}
targetId={String(listing._id)}
targetType="listing"
onReportSuccess={() => setFlash(REPORT_SUBMITTED_MESSAGE)}
/>
</Animated.View>

Expand Down
5 changes: 4 additions & 1 deletion frontend/app/listings/[id]/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { Id } from 'convex/_generated/dataModel';
import ImageUploader from '@/components/ImageUploader';
import ListingUnavailable from '../../../components/ListingUnavailable';
import TagInput from '../../../components/TagInput';
import { useFlash } from '../../../contexts/FlashContext';
import { useEntranceAnimation } from '../../../hooks/useEntranceAnimation';
import { borderRadius, colors, spacing, typography } from '../../../theme/tokens';

Expand Down Expand Up @@ -53,6 +54,7 @@ function getListingActionError(error: unknown, fallbackTitle: string) {

export default function EditListingScreen() {
const router = useRouter();
const { setFlash } = useFlash();
const { id } = useLocalSearchParams<{ id?: string | string[] }>();
const listingId = typeof id === 'string' && id.trim().length > 0 ? id : null;
const listing = useQuery(
Expand Down Expand Up @@ -149,7 +151,8 @@ export default function EditListingScreen() {
);
return;
}
showAlert('Success', 'Listing updated.', () => router.back());
setFlash('Listing updated.');
router.back();
} catch (error) {
const actionError = getListingActionError(error, 'Update failed');
showAlert(actionError.title, actionError.message);
Expand Down
5 changes: 4 additions & 1 deletion frontend/app/listings/new.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { useRouter } from 'expo-router';
import { api } from 'convex/_generated/api';
import ImageUploader from '@/components/ImageUploader';
import TagInput from '../../components/TagInput';
import { useFlash } from '../../contexts/FlashContext';
import { useAuth } from '../../hooks/useAuth';
import { useEntranceAnimation } from '../../hooks/useEntranceAnimation';
import { colors, typography, borderRadius, spacing } from '../../theme/tokens';
Expand Down Expand Up @@ -61,6 +62,7 @@ function getListingActionError(error: unknown, fallbackTitle: string) {

export default function NewListingScreen() {
const router = useRouter();
const { setFlash } = useFlash();
const createListing = useAction(api.listings.createListing);
const { isAuthenticated, isSessionLoading } = useAuth();
const entranceStyle = useEntranceAnimation();
Expand Down Expand Up @@ -144,7 +146,8 @@ export default function NewListingScreen() {
images,
tags,
});
showAlert('Success', 'Listing created.', () => router.replace('/'));
setFlash('Listing created.');
router.replace('/');
} catch (error) {
const actionError = getListingActionError(error, 'Create failed');
showAlert(actionError.title, actionError.message);
Expand Down
4 changes: 4 additions & 0 deletions frontend/app/profile/[userId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import { useLocalSearchParams, useRouter } from 'expo-router';
import { useQuery } from 'convex/react';
import { api } from 'convex/_generated/api';
import type { Doc } from 'convex/_generated/dataModel';
import { useFlash } from '../../contexts/FlashContext';
import { useAuth } from '../../hooks/useAuth';
import { useResolvedImageUrls } from '../../hooks/useResolvedImageUrls';
import ListingCard from '../../components/ListingCard';
import { ReportModal } from '../../components/ReportModal';
import { ScreenState } from '../../components/ScreenState';
import { REPORT_SUBMITTED_MESSAGE } from '../../constants/feedbackMessages';
import { colors, typography, spacing, borderRadius } from '../../theme/tokens';

function yearToOrdinal(gradYear: number): string {
Expand All @@ -30,6 +32,7 @@ function yearToOrdinal(gradYear: number): string {
export default function PublicProfileScreen() {
const { userId: rawUserId } = useLocalSearchParams<{ userId?: string }>();
const router = useRouter();
const { setFlash } = useFlash();
const { width } = useWindowDimensions();
const { user, isAuthenticated } = useAuth();
const [reportOpen, setReportOpen] = useState(false);
Expand Down Expand Up @@ -138,6 +141,7 @@ export default function PublicProfileScreen() {
onClose={() => setReportOpen(false)}
targetId={profile._id}
targetType="profile"
onReportSuccess={() => setFlash(REPORT_SUBMITTED_MESSAGE)}
/>

<Text style={styles.sectionTitle}>Listings</Text>
Expand Down
20 changes: 18 additions & 2 deletions frontend/components/ReportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { Alert, Modal, Pressable, StyleSheet, Text, TextInput, View } from 'reac
import { colors } from '../theme/tokens';
import { useMutation } from 'convex/react';
import { api } from 'convex/_generated/api';
import {
REPORT_SUBMITTED_ALERT_BODY,
REPORT_SUBMITTED_ALERT_TITLE,
} from '../constants/feedbackMessages';

type ReportReason = 'scam' | 'inappropriate' | 'spam';

Expand All @@ -11,6 +15,8 @@ type ReportModalProps = {
onClose: () => void;
targetId: string;
targetType: 'listing' | 'profile';
/** When set, success uses this instead of a blocking alert (e.g. in-app flash banner). */
onReportSuccess?: () => void;
};

const REASONS: { value: ReportReason; label: string; desc: string }[] = [
Expand All @@ -21,7 +27,13 @@ const REASONS: { value: ReportReason; label: string; desc: string }[] = [

const MAX_NOTES = 500;

export function ReportModal({ isVisible, onClose, targetId, targetType }: ReportModalProps) {
export function ReportModal({
isVisible,
onClose,
targetId,
targetType,
onReportSuccess,
}: ReportModalProps) {
const createReport = useMutation(api.reports.createReport);
const [reason, setReason] = useState<ReportReason | null>(null);
const [notes, setNotes] = useState('');
Expand All @@ -48,7 +60,11 @@ export function ReportModal({ isVisible, onClose, targetId, targetType }: Report
reason,
notes: notes.trim() ? notes.trim() : undefined,
});
Alert.alert('Report submitted', 'Thanks for helping keep PolyBuys safe.');
if (onReportSuccess) {
onReportSuccess();
} else {
Alert.alert(REPORT_SUBMITTED_ALERT_TITLE, REPORT_SUBMITTED_ALERT_BODY);
}
handleClose();
} catch (err) {
const msg = String((err as Error)?.message ?? 'Unable to submit report right now.');
Expand Down
7 changes: 7 additions & 0 deletions frontend/constants/feedbackMessages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/** User-facing copy shared across flash banners, alerts, and modals. */

export const REPORT_SUBMITTED_ALERT_TITLE = 'Report submitted';
export const REPORT_SUBMITTED_ALERT_BODY = 'Thanks for helping keep PolyBuys safe.';

/** Full sentence for in-app flash after a report succeeds. */
export const REPORT_SUBMITTED_MESSAGE = `${REPORT_SUBMITTED_ALERT_TITLE}. ${REPORT_SUBMITTED_ALERT_BODY}`;
Loading
Loading