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 */}
+
+
+
+
+ {/* Manual fallback */}
+
+
+
+
+
+
+
+
+ );
+});
+
+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,
+ },
+});
\ No newline at end of file
diff --git a/frontend/app/(app)/trips/[id]/activities/components/add-activity-manual-sheet.tsx b/frontend/app/(app)/trips/[id]/activities/components/add-activity-manual-sheet.tsx
new file mode 100644
index 00000000..7b7e4c34
--- /dev/null
+++ b/frontend/app/(app)/trips/[id]/activities/components/add-activity-manual-sheet.tsx
@@ -0,0 +1,433 @@
+import { useCreateActivity } from "@/api/activities";
+import { useUploadImage } from "@/api/files/custom";
+import {
+ Box,
+ Button,
+ DateRangePicker,
+ Dialog,
+ ImagePicker,
+ Text,
+ useToast,
+} from "@/design-system";
+import BottomSheet from "@/design-system/components/bottom-sheet/bottom-sheet";
+import type { DateRange } from "@/design-system/primitives/date-picker";
+import { ColorPalette } from "@/design-system/tokens/color";
+import { CornerRadius } from "@/design-system/tokens/corner-radius";
+import { Layout } from "@/design-system/tokens/layout";
+import { getImageURL } from "@/services/imageService";
+import type {
+ ModelsActivityAPIResponse,
+ ModelsParsedActivityData,
+} from "@/types/types.gen";
+import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
+import { BottomSheetMethods } from "@gorhom/bottom-sheet/lib/typescript/types";
+import { router } from "expo-router";
+import {
+ Calendar,
+ DollarSign,
+ Link,
+ MapPin,
+ Plus,
+} from "lucide-react-native";
+import {
+ forwardRef,
+ useCallback,
+ useImperativeHandle,
+ useRef,
+ useState,
+} from "react";
+import { Pressable, StyleSheet, TextInput } from "react-native";
+import { CategoriesSheet } from "./categories-sheet";
+import { FormRow } from "./form-row";
+
+// ─── Types ───────────────────────────────────────────────────────────────────
+
+type PendingLocation = { name: string; lat: number; lng: number };
+
+export type AddActivityManualSheetHandle = {
+ open: (prefill?: Partial) => void;
+ close: () => void;
+ setLocation: (loc: PendingLocation) => void;
+};
+
+type AddActivityManualSheetProps = {
+ tripID: string;
+ onSaved: (activity: ModelsActivityAPIResponse) => void;
+ onClose: () => void;
+};
+
+// ─── Helpers ─────────────────────────────────────────────────────────────────
+
+function formatDateRange(range: DateRange): string | null {
+ if (!range.start) return null;
+ const fmt = (d: Date) =>
+ d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
+ if (!range.end || range.start.getTime() === range.end.getTime())
+ return fmt(range.start);
+ return `${fmt(range.start)} – ${fmt(range.end)}`;
+}
+
+// ─── Component ───────────────────────────────────────────────────────────────
+
+export const AddActivityManualSheet = forwardRef<
+ AddActivityManualSheetHandle,
+ AddActivityManualSheetProps
+>(({ tripID, onSaved, onClose }, ref) => {
+ const toast = useToast();
+ const createActivity = useCreateActivity();
+ const uploadImage = useUploadImage();
+ const bottomSheetRef = useRef(null);
+ const categoriesSheetRef = useRef(null);
+ const savedRef = useRef(false);
+
+ // ─── Form state ──────────────────────────────────────────────────────────
+ const [thumbnailUri, setThumbnailUri] = useState(null);
+ const [name, setName] = useState("");
+ const [description, setDescription] = useState("");
+ const [categories, setCategories] = useState([]);
+ const [locationName, setLocationName] = useState(null);
+ const [locationLat, setLocationLat] = useState(null);
+ const [locationLng, setLocationLng] = useState(null);
+ const [price, setPrice] = useState("");
+ const [dateRange, setDateRange] = useState({ start: null, end: null });
+ const [link, setLink] = useState("");
+ const [isDatePickerVisible, setIsDatePickerVisible] = useState(false);
+ const [isSaving, setIsSaving] = useState(false);
+ const [showCancelConfirm, setShowCancelConfirm] = useState(false);
+
+ // ─── Imperative handle ───────────────────────────────────────────────────
+ useImperativeHandle(ref, () => ({
+ open: (prefill) => {
+ if (prefill) {
+ if (prefill.name) setName(prefill.name);
+ if (prefill.description) setDescription(prefill.description);
+ if (prefill.thumbnail_url) setThumbnailUri(prefill.thumbnail_url);
+ if (prefill.media_url) setLink(prefill.media_url);
+ }
+ bottomSheetRef.current?.snapToIndex(0);
+ },
+ close: () => bottomSheetRef.current?.close(),
+ setLocation: (loc) => {
+ setLocationName(loc.name);
+ setLocationLat(loc.lat);
+ setLocationLng(loc.lng);
+ },
+ }));
+
+ // ─── Handlers ────────────────────────────────────────────────────────────
+
+ const handleLocationPress = useCallback(() => {
+ router.push(
+ `/trips/${tripID}/search-location?tripID=${tripID}&returnTo=/trips/${tripID}/activities`,
+ );
+ }, [tripID]);
+
+ const resetForm = () => {
+ setName("");
+ setDescription("");
+ setThumbnailUri(null);
+ setCategories([]);
+ setLocationName(null);
+ setLocationLat(null);
+ setLocationLng(null);
+ setPrice("");
+ setDateRange({ start: null, end: null });
+ setLink("");
+ };
+
+ const handleSave = async () => {
+ if (!name.trim()) return;
+ setIsSaving(true);
+ try {
+ let thumbnailURL: string | undefined;
+
+ if (thumbnailUri?.startsWith("http")) {
+ // Remote URL from autofill — use directly
+ thumbnailURL = thumbnailUri;
+ } else if (thumbnailUri) {
+ // Local file — upload and get presigned URL
+ try {
+ const res = await uploadImage.mutateAsync({
+ uri: thumbnailUri,
+ sizes: ["medium"],
+ });
+ const urlRes = await getImageURL(res.imageId, "medium");
+ thumbnailURL = urlRes.url;
+ } catch (uploadErr) {
+ console.warn("Image upload failed, skipping thumbnail:", uploadErr);
+ }
+ }
+
+ const dates =
+ dateRange.start && dateRange.end
+ ? [
+ {
+ start: dateRange.start.toISOString().split("T")[0]!,
+ end: dateRange.end.toISOString().split("T")[0]!,
+ },
+ ]
+ : undefined;
+
+ const estimatedPrice = parseFloat(price);
+ const result = await createActivity.mutateAsync({
+ tripID,
+ data: {
+ name: name.trim(),
+ description: description.trim() || undefined,
+ category_names: categories,
+ location_name: locationName ?? undefined,
+ location_lat: locationLat ?? undefined,
+ location_lng: locationLng ?? undefined,
+ estimated_price: isNaN(estimatedPrice) ? undefined : estimatedPrice,
+ dates,
+ thumbnail_url: thumbnailURL,
+ media_url: link.trim() || undefined,
+ },
+ });
+
+ toast.show({
+ message: "Activity added",
+ action: { label: "View", onPress: () => {} },
+ });
+ savedRef.current = true;
+ resetForm();
+ onSaved(result);
+ } catch (e) {
+ toast.show({ message: "Couldn't save activity. Try again." });
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (savedRef.current) {
+ savedRef.current = false;
+ return;
+ }
+ const hasData = name.trim() || description.trim() || thumbnailUri;
+ if (hasData) {
+ setShowCancelConfirm(true);
+ } else {
+ resetForm();
+ onClose();
+ }
+ };
+
+ const isValid = name.trim().length > 0;
+
+ // ─── Render ──────────────────────────────────────────────────────────────
+
+ return (
+ <>
+
+
+
+
+ }
+ >
+
+
+
+ Add an activity
+
+
+
+
+ setThumbnailUri(uri)}
+ placeholder="Add cover image"
+ />
+
+
+
+
+
+
+
+
+
+ {categories.map((cat) => (
+
+ {cat}
+
+ ))}
+ categoriesSheetRef.current?.snapToIndex(0)}>
+
+
+ Add category
+
+
+
+
+
+
+
+
+
+
+
+ setIsDatePickerVisible(true)}
+ />
+
+
+
+
+
+
+
+
+ categoriesSheetRef.current?.close()}
+ />
+
+ setIsDatePickerVisible(false)}
+ onSave={(range) => {
+ setDateRange(range);
+ setIsDatePickerVisible(false);
+ }}
+ initialRange={dateRange}
+ />
+
+