diff --git a/frontend/app/(app)/trips/[id]/activities/components/activity-card.tsx b/frontend/app/(app)/trips/[id]/activities/components/activity-card.tsx new file mode 100644 index 00000000..c9efa63f --- /dev/null +++ b/frontend/app/(app)/trips/[id]/activities/components/activity-card.tsx @@ -0,0 +1,145 @@ +import { Box, Text } from "@/design-system"; +import { Avatar } from "@/design-system/components/avatars/avatar"; +import { ColorPalette } from "@/design-system/tokens/color"; +import { CornerRadius } from "@/design-system/tokens/corner-radius"; +import { Elevation } from "@/design-system/tokens/elevation"; +import { Layout } from "@/design-system/tokens/layout"; +import type { ModelsActivityAPIResponse } from "@/types/types.gen"; +import { Image } from "expo-image"; +import { Heart } from "lucide-react-native"; +import { Pressable, StyleSheet } from "react-native"; + +type ActivityCardProps = { + activity: ModelsActivityAPIResponse; + onPress?: () => void; + isNew?: boolean; +}; + +export function ActivityCard({ activity, onPress, isNew }: ActivityCardProps) { + const hasThumbnail = !!activity.thumbnail_url; + const goingCount = activity.going_count ?? 0; + const goingUsers = activity.going_users ?? []; + + return ( + ({ opacity: pressed ? 0.97 : 1 })} + > + + {/* Thumbnail */} + {hasThumbnail && ( + + + {isNew && ( + + + New • 1m + + + )} + + )} + + {/* Content */} + + + + {activity.name} + + + {goingCount > 0 && ( + + + + {goingCount} + + + )} + + + + {activity.estimated_price != null && ( + + ${activity.estimated_price} per person + + )} + + {goingUsers.length > 0 && ( + + {goingUsers.slice(0, 3).map((u) => ( + + ))} + + )} + + + {/* Comments row — placeholder, comments ticket is separate */} + {isNew && ( + + + 1 new comment + + + )} + + + ); +} + +const styles = StyleSheet.create({ + card: { + backgroundColor: ColorPalette.white, + borderRadius: CornerRadius.lg, + overflow: "hidden", + ...Elevation.xs, + }, + thumbnailContainer: { + height: 160, + width: "100%", + position: "relative", + }, + thumbnail: { + width: "100%", + height: "100%", + }, + newBadge: { + position: "absolute", + top: Layout.spacing.xs, + left: Layout.spacing.xs, + backgroundColor: ColorPalette.brand500, + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: CornerRadius.sm, + }, + content: { + padding: Layout.spacing.xs, + gap: 4, + }, + commentRow: { + paddingHorizontal: Layout.spacing.xs, + paddingVertical: Layout.spacing.xs, + backgroundColor: ColorPalette.blue50, + }, +}); \ No newline at end of file diff --git a/frontend/app/(app)/trips/[id]/activities/components/add-activity-entry-sheet.tsx b/frontend/app/(app)/trips/[id]/activities/components/add-activity-entry-sheet.tsx new file mode 100644 index 00000000..a1ff6677 --- /dev/null +++ b/frontend/app/(app)/trips/[id]/activities/components/add-activity-entry-sheet.tsx @@ -0,0 +1,190 @@ +import { parseActivityLink } from "@/api/activities"; +import { Box, Button, Text, TextField, useToast } from "@/design-system"; +import { ColorPalette } from "@/design-system/tokens/color"; +import { CornerRadius } from "@/design-system/tokens/corner-radius"; +import { Layout } from "@/design-system/tokens/layout"; +import type { ModelsParsedActivityData } from "@/types/types.gen"; +import { Link } from "lucide-react-native"; +import { forwardRef, useImperativeHandle, useState } from "react"; +import { + Image, + KeyboardAvoidingView, + Modal, + Platform, + Pressable, + StyleSheet, +} from "react-native"; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +type AddActivityEntrySheetProps = { + tripID: string; + onManual: () => void; + onAutofilled: (data: ModelsParsedActivityData) => void; + onClose: () => void; +}; + +export type AddActivityEntrySheetHandle = { + open: () => void; + close: () => void; +}; + +// ─── Component ─────────────────────────────────────────────────────────────── + +export const AddActivityEntrySheet = forwardRef< + AddActivityEntrySheetHandle, + AddActivityEntrySheetProps +>(({ tripID, onManual, onAutofilled, onClose }, ref) => { + const toast = useToast(); + const [visible, setVisible] = useState(false); + const [url, setUrl] = useState(""); + const [isAutofilling, setIsAutofilling] = useState(false); + + useImperativeHandle(ref, () => ({ + open: () => setVisible(true), + close: () => { + setVisible(false); + setUrl(""); + }, + })); + + const handleClose = () => { + setVisible(false); + setUrl(""); + onClose(); + }; + + const handleManual = () => { + setVisible(false); + setUrl(""); + onManual(); + }; + + const handleAutofill = async () => { + if (!url.trim()) return; + setIsAutofilling(true); + try { + const data = await parseActivityLink(tripID, { url: url.trim() }); + setIsAutofilling(false); + setVisible(false); + setUrl(""); + onAutofilled(data); + } catch { + setIsAutofilling(false); + toast.show({ message: "Couldn't fetch that link. Try adding manually." }); + } + }; + + return ( + + + + {/* Prevent backdrop tap from closing when tapping sheet */} + {}}> + {/* Handle */} + + + + + + {/* Illustration */} + + + {/* Header — changes during autofill */} + + + {isAutofilling + ? "Fetching listing details..." + : "Add an activity"} + + + {isAutofilling + ? "Hang tight while we pull the details from your link. This only takes a second or two." + : "Easily import from yelp, instagram, tiktok, etc."} + + + + {/* URL input */} + + } + autoCapitalize="none" + keyboardType="url" + /> + + + {/* Autofill button */} + +