Skip to content
2 changes: 1 addition & 1 deletion apps/expo/app/(app)/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ const getTripNewOptions = (t: TranslationFunction) =>

const getTripEditOptions = (t: TranslationFunction) =>
({
title: t('packs.editPack'),
title: t('trips.editTrip'),
presentation: 'modal',
animation: 'slide_from_bottom',
}) as const;
Expand Down
13 changes: 13 additions & 0 deletions apps/expo/features/trips/components/TripCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Alert, type AlertRef, Button } from '@packrat/ui/nativewindui';
import { Icon } from '@roninoss/icons';
import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { formatLocalDate } from 'expo-app/lib/utils/dateUtils';
import { useRouter } from 'expo-router';
import { useRef } from 'react';
import { Pressable, Text, View } from 'react-native';
Expand Down Expand Up @@ -98,6 +99,18 @@ export function TripCard({ trip, onPress }: TripCardProps) {
</Button>
</View>

{/* Dates */}
{(trip.startDate != null || trip.endDate != null) && (
<View className="flex-row items-center mt-1">
<Icon name="calendar-month" size={14} color={colors.primary} />
<Text className="ml-1 text-sm text-muted-foreground">
{trip.startDate != null && trip.endDate != null
? `${formatLocalDate(trip.startDate)} → ${formatLocalDate(trip.endDate)}`
: formatLocalDate(trip.startDate ?? trip.endDate)}
</Text>
</View>
)}

{/* Description */}
{trip.description && (
<Text className="text-sm text-muted-foreground mt-2" numberOfLines={2}>
Expand Down
14 changes: 12 additions & 2 deletions apps/expo/features/trips/components/UpcomingTripsTile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Icon } from '@roninoss/icons';
import { featureFlags } from 'expo-app/config';
import { useTrips } from 'expo-app/features/trips/hooks';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { parseLocalDate } from 'expo-app/lib/utils/dateUtils';
import { useRouter } from 'expo-router';
import { useMemo, useRef } from 'react';
import { View } from 'react-native';
Expand All @@ -16,9 +17,18 @@ export function UpcomingTripsTile() {
// ✅ get all trips
const trips = useTrips();

// ✅ derive upcoming trips (in future)
// ✅ derive upcoming trips (today or in future)
const upcomingTrips = useMemo(
() => trips.filter((t) => t.startDate && new Date(t.startDate) > new Date()),
() =>
trips.filter((t) => {
if (!t.startDate) return false;
const parsed = parseLocalDate(t.startDate);
if (parsed == null) return false;
// Compare against start-of-today so same-day trips are included
const startOfToday = new Date();
startOfToday.setHours(0, 0, 0, 0);
return parsed >= startOfToday;
}),
[trips],
);

Expand Down
2 changes: 2 additions & 0 deletions apps/expo/features/trips/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './hooks';
export * from './types';
26 changes: 23 additions & 3 deletions apps/expo/features/trips/screens/TripListScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useColorScheme } from 'expo-app/lib/hooks/useColorScheme';
import { useTranslation } from 'expo-app/lib/hooks/useTranslation';
import { asNonNullableRef } from 'expo-app/lib/utils/asNonNullableRef';
import { Link, useRouter } from 'expo-router';
import { useCallback, useRef } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { FlatList, Pressable, SafeAreaView, Text, TouchableOpacity, View } from 'react-native';
import { TripCard } from '../components/TripCard';
import { useTrips } from '../hooks';
Expand All @@ -26,6 +26,19 @@ export function TripsListScreen() {
const { t } = useTranslation();
const trips = useTrips();
const searchBarRef = useRef<LargeTitleSearchBarRef>(null);
const [searchValue, setSearchValue] = useState('');

const filteredTrips = useMemo(() => {
const trimmed = searchValue.trim();
if (!trimmed) return trips;
const lower = trimmed.toLowerCase();
return trips.filter(
(trip) =>
trip.name.toLowerCase().includes(lower) ||
(trip.description ?? '').toLowerCase().includes(lower) ||
(trip.location?.name ?? '').toLowerCase().includes(lower),
);
}, [trips, searchValue]);

const handleTripPress = useCallback(
(trip: Trip) => {
Expand All @@ -39,6 +52,13 @@ export function TripsListScreen() {
};

const renderEmptyState = () => {
if (searchValue.trim() && trips.length > 0) {
return (
<View className="flex-1 items-center justify-center p-8">
<Text className="text-center text-muted-foreground">{t('trips.noSearchResults')}</Text>
</View>
);
}
return (
<View className="flex-1 items-center justify-center p-8">
<View className="mb-4 rounded-full bg-muted p-4">
Expand All @@ -61,7 +81,7 @@ export function TripsListScreen() {
searchBar={{
iosHideWhenScrolling: true,
ref: asNonNullableRef(searchBarRef),
onChangeText() {}, // no search filtering
onChangeText: setSearchValue,
content: (
<View className="flex-1 items-center justify-center">
<Text>{t('trips.searchTrips')}</Text>
Expand All @@ -76,7 +96,7 @@ export function TripsListScreen() {
/>

<FlatList
data={trips || []}
data={filteredTrips}
keyExtractor={(trip) => trip.id}
renderItem={({ item: trip }) => (
<View className="px-4 pt-4">
Expand Down
2 changes: 1 addition & 1 deletion apps/expo/features/trips/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface Trip {
localUpdatedAt?: string;
}

export type TripInStore = Omit<Trip, 'trips'>;
export type TripInStore = Trip;

export type TripInput = Omit<
TripInStore,
Expand Down
1 change: 1 addition & 0 deletions apps/expo/lib/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,7 @@
"tripLocation": "Trip Location",
"noTrips": "No trips available",
"noTripsFound": "No trips found",
"noSearchResults": "No trips match your search.",
"noTripsYet": "You haven't created any trips yet.",
"createNewTrip": "Create New Trip",
"upcomingTrips": "Upcoming Trips",
Expand Down
39 changes: 39 additions & 0 deletions apps/expo/lib/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,45 @@ export type TranslationKeys =
| 'trips.notStarted'
| 'trips.complete'
| 'trips.inProgress'
| 'trips.addLocation'
| 'trips.createNewTrip'
| 'trips.createTripsToSee'
| 'trips.creating'
| 'trips.dates'
| 'trips.deleteTrip'
| 'trips.deleteTripConfirmation'
| 'trips.description'
| 'trips.details'
| 'trips.editTrip'
| 'trips.endDate'
| 'trips.gotIt'
| 'trips.items'
| 'trips.location'
| 'trips.newTrip'
| 'trips.noDetailsAvailable'
| 'trips.noPackLinked'
| 'trips.noPackSelected'
| 'trips.noSearchResults'
| 'trips.noTripsFound'
| 'trips.noTripsYet'
| 'trips.noTripsYetTitle'
| 'trips.openInMaps'
| 'trips.pack'
| 'trips.searchTrips'
| 'trips.selectDate'
| 'trips.selectPack'
| 'trips.startDate'
| 'trips.totalWeight'
| 'trips.trailConditions'
| 'trips.trailConditionsMessage'
| 'trips.tripCreatedSuccess'
| 'trips.tripUpdatedSuccess'
| 'trips.trips'
| 'trips.upcomingTrips'
| 'trips.updateTrip'
| 'trips.updating'
| 'trips.viewDetails'
| 'trips.viewPack'
// Catalog
| 'catalog.itemsCatalog'
| 'catalog.searchItems'
Expand Down
32 changes: 32 additions & 0 deletions apps/expo/lib/utils/dateUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Parse a date string, handling YYYY-MM-DD strings as local dates
* instead of UTC (which is the default `new Date('YYYY-MM-DD')` behavior).
*
* Returns `null` for missing or invalid input.
*/
export function parseLocalDate(dateString?: string): Date | null {
if (!dateString) return null;
const dateOnlyMatch = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateString);
if (dateOnlyMatch) {
const year = Number(dateOnlyMatch[1]);
const month = Number(dateOnlyMatch[2]);
const day = Number(dateOnlyMatch[3]);
const date = new Date(year, month - 1, day);
if (Number.isNaN(date.getTime())) return null;
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) {
return null;
}
return date;
}
const date = new Date(dateString);
return Number.isNaN(date.getTime()) ? null : date;
}

/**
* Format a date string for display, returning an em-dash for missing or
* invalid values. Uses the user's locale via `toLocaleDateString()`.
*/
export function formatLocalDate(dateString?: string): string {
const date = parseLocalDate(dateString);
return date ? date.toLocaleDateString() : '\u2014';
}