-
Notifications
You must be signed in to change notification settings - Fork 0
activity creation flow #262
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <Pressable | ||
| onPress={onPress} | ||
| style={({ pressed }) => ({ opacity: pressed ? 0.97 : 1 })} | ||
| > | ||
| <Box style={styles.card}> | ||
| {/* Thumbnail */} | ||
| {hasThumbnail && ( | ||
| <Box style={styles.thumbnailContainer}> | ||
| <Image | ||
| source={{ uri: activity.thumbnail_url! }} | ||
| style={styles.thumbnail} | ||
| contentFit="cover" | ||
| /> | ||
| {isNew && ( | ||
| <Box style={styles.newBadge}> | ||
| <Text variant="bodyXxsMedium" color="white"> | ||
| New • 1m | ||
| </Text> | ||
| </Box> | ||
| )} | ||
| </Box> | ||
| )} | ||
|
|
||
| {/* Content */} | ||
| <Box style={styles.content}> | ||
| <Box | ||
| flexDirection="row" | ||
| alignItems="center" | ||
| justifyContent="space-between" | ||
| > | ||
| <Text | ||
| variant="bodySmMedium" | ||
| color="gray900" | ||
| style={{ flex: 1 }} | ||
| numberOfLines={1} | ||
| > | ||
| {activity.name} | ||
| </Text> | ||
| <Box flexDirection="row" alignItems="center" gap="xs"> | ||
| {goingCount > 0 && ( | ||
| <Box flexDirection="row" alignItems="center" gap="xxs"> | ||
| <Heart | ||
| size={14} | ||
| color={ColorPalette.statusError} | ||
| fill={ColorPalette.statusError} | ||
| /> | ||
| <Text variant="bodyXsMedium" color="gray500"> | ||
| {goingCount} | ||
| </Text> | ||
| </Box> | ||
| )} | ||
| </Box> | ||
| </Box> | ||
|
|
||
| {activity.estimated_price != null && ( | ||
| <Text variant="bodyXsDefault" color="gray500"> | ||
| ${activity.estimated_price} per person | ||
| </Text> | ||
| )} | ||
|
|
||
| {goingUsers.length > 0 && ( | ||
| <Box flexDirection="row" alignItems="center" gap="xxs"> | ||
| {goingUsers.slice(0, 3).map((u) => ( | ||
| <Avatar | ||
| key={u.user_id} | ||
| variant="xs" | ||
| seed={u.user_id} | ||
| profilePhoto={u.profile_picture_url ?? undefined} | ||
| /> | ||
| ))} | ||
| </Box> | ||
| )} | ||
| </Box> | ||
|
|
||
| {/* Comments row — placeholder, comments ticket is separate */} | ||
| {isNew && ( | ||
| <Box style={styles.commentRow}> | ||
| <Text variant="bodyXsMedium" color="blue500"> | ||
| 1 new comment | ||
| </Text> | ||
| </Box> | ||
| )} | ||
| </Box> | ||
| </Pressable> | ||
| ); | ||
| } | ||
|
|
||
| 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, | ||
| }, | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }; | ||
|
Comment on lines
+51
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not apply autofill results after the sheet is dismissed. Backdrop close stays active while Also applies to: 63-75, 85-85 🤖 Prompt for AI Agents |
||
|
|
||
| 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 ( | ||
| <Modal | ||
| visible={visible} | ||
| transparent | ||
| animationType="slide" | ||
| onRequestClose={handleClose} | ||
| > | ||
| <Pressable style={styles.backdrop} onPress={handleClose}> | ||
| <KeyboardAvoidingView | ||
| behavior={Platform.OS === "ios" ? "padding" : undefined} | ||
| style={styles.sheetWrapper} | ||
| > | ||
| {/* Prevent backdrop tap from closing when tapping sheet */} | ||
| <Pressable style={styles.sheet} onPress={() => {}}> | ||
| {/* Handle */} | ||
| <Box alignItems="center" paddingTop="sm" paddingBottom="xs"> | ||
| <Box style={styles.handle} /> | ||
| </Box> | ||
|
|
||
| <Box padding="sm" gap="md" alignItems="center"> | ||
| {/* Illustration */} | ||
| <Image | ||
| source={require("@/assets/images/binoculars.png")} | ||
| style={{ width: 80, height: 80 }} | ||
| resizeMode="contain" | ||
| /> | ||
|
|
||
| {/* Header — changes during autofill */} | ||
| <Box gap="xxs" alignItems="center"> | ||
| <Text variant="headingSm" color="gray900"> | ||
| {isAutofilling | ||
| ? "Fetching listing details..." | ||
| : "Add an activity"} | ||
| </Text> | ||
| <Text | ||
| variant="bodySmDefault" | ||
| color="gray500" | ||
| textAlign="center" | ||
| > | ||
| {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."} | ||
| </Text> | ||
| </Box> | ||
|
|
||
| {/* URL input */} | ||
| <Box style={{ width: "100%" }}> | ||
| <TextField | ||
| value={url} | ||
| onChangeText={setUrl} | ||
| placeholder="https://..." | ||
| leftIcon={<Link size={16} color={ColorPalette.gray400} />} | ||
| autoCapitalize="none" | ||
| keyboardType="url" | ||
| /> | ||
| </Box> | ||
|
|
||
| {/* Autofill button */} | ||
| <Box style={{ width: "100%" }}> | ||
| <Button | ||
| layout="textOnly" | ||
| label={isAutofilling ? "Autofilling..." : "Autofill from link"} | ||
| variant="Primary" | ||
| loading={isAutofilling} | ||
| disabled={!url.trim() || isAutofilling} | ||
| onPress={handleAutofill} | ||
| /> | ||
| </Box> | ||
|
|
||
| {/* Manual fallback */} | ||
| <Box style={{ width: "100%" }}> | ||
| <Button | ||
| layout="textOnly" | ||
| label="Add manually" | ||
| variant="Secondary" | ||
| disabled={isAutofilling} | ||
| onPress={handleManual} | ||
| /> | ||
| </Box> | ||
| </Box> | ||
| </Pressable> | ||
| </KeyboardAvoidingView> | ||
| </Pressable> | ||
| </Modal> | ||
| ); | ||
| }); | ||
|
|
||
| AddActivityEntrySheet.displayName = "AddActivityEntrySheet"; | ||
|
|
||
| // ─── Styles ────────────────────────────────────────────────────────────────── | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| backdrop: { | ||
| flex: 1, | ||
| backgroundColor: "rgba(0,0,0,0.4)", | ||
| justifyContent: "flex-end", | ||
| }, | ||
| sheetWrapper: { | ||
| justifyContent: "flex-end", | ||
| }, | ||
| sheet: { | ||
| backgroundColor: ColorPalette.white, | ||
| borderTopLeftRadius: CornerRadius.lg, | ||
| borderTopRightRadius: CornerRadius.lg, | ||
| paddingBottom: Layout.spacing.xl, | ||
| }, | ||
| handle: { | ||
| width: 36, | ||
| height: 4, | ||
| borderRadius: 2, | ||
| backgroundColor: ColorPalette.gray300, | ||
| }, | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoded placeholder text when
isNewis true.Lines 40 and 101 display static strings ("New • 1m", "1 new comment") that don't reflect actual activity data. If
isNewis meant to indicate genuinely new activities, the badge timestamp and comment count should come from the activity model. If these are intentional placeholders, consider removing them until the feature is implemented.Also applies to: 98-104
🤖 Prompt for AI Agents