Files
Dhairyashil ShindeandGitHub fa53f54815 feat(companion): add configurable landing page (#27267)
* feat(companion): add configurable landing page feature

- Add useUserPreferences hook for persistent storage of landing page preference
- Add LandingPagePicker component for both iOS and Android/web platforms
- Update tabs index to redirect based on user preference
- Update bookings index to accept initial filter from URL params
- Add App Settings section in More screen with landing page selector
- Clear user preferences on logout for fresh state

* fix(companion): remove try-finally for React Compiler compatibility

* fix(companion): use router.replace for dynamic landing page redirect

Replace Redirect component with router.replace() to fix TypeScript
strict typing issue with expo-router's Href type for dynamic routes.

* fix(companion): use literal route strings for TypeScript strict typing

Use switch statement with literal route strings instead of dynamic
string variable to satisfy expo-router's strict Href type checking.

* working fix

* better css
2026-02-13 11:28:42 -03:00

222 lines
7.7 KiB
TypeScript

import { Ionicons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router";
import { useState, useEffect, useRef } from "react";
import { Text, TextInput, useColorScheme, View } from "react-native";
import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen";
import { Header } from "@/components/Header";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { AppPressable } from "@/components/AppPressable";
import { type BookingFilter, useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
import { useEventTypes } from "@/hooks";
import { getColors } from "@/constants/colors";
const VALID_FILTERS: BookingFilter[] = [
"upcoming",
"unconfirmed",
"recurring",
"past",
"cancelled",
];
function isValidBookingFilter(value: string | undefined): value is BookingFilter {
return value !== undefined && VALID_FILTERS.includes(value as BookingFilter);
}
export default function Bookings() {
const { filter } = useLocalSearchParams<{ filter?: string }>();
const initialFilter = isValidBookingFilter(filter) ? filter : "upcoming";
const [searchQuery, setSearchQuery] = useState("");
const [selectedEventTypeId, setSelectedEventTypeId] = useState<number | null>(null);
const [selectedEventTypeLabel, setSelectedEventTypeLabel] = useState<string | null>(null);
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const theme = getColors(isDark);
// Use React Query hook for event types (same as iOS for unified caching)
const { data: eventTypes = [], isLoading: eventTypesLoading } = useEventTypes();
// Use the active booking filter hook
const {
activeFilter,
filterOptions,
filterParams,
handleFilterChange: originalHandleFilterChange,
} = useActiveBookingFilter(initialFilter, () => {
// Clear dependent filters when status filter changes
setSearchQuery("");
setSelectedEventTypeId(null);
setSelectedEventTypeLabel(null);
});
// Wrap handleFilterChange with debug logging
const handleFilterChange = (filter: string) => {
originalHandleFilterChange(filter as BookingFilter);
};
// Track if we want to ignore the next activeFilter change (because it came from URL sync)
const lastUrlFilter = useRef<string | null>(null);
// Reactively update filter when URL params change (e.g., from deep link)
useEffect(() => {
if (
isValidBookingFilter(filter) &&
filter !== activeFilter &&
filter !== lastUrlFilter.current
) {
lastUrlFilter.current = filter;
originalHandleFilterChange(filter);
}
}, [filter, activeFilter, originalHandleFilterChange]);
const handleSearch = (query: string) => {
setSearchQuery(query);
};
const clearEventTypeFilter = () => {
setSelectedEventTypeId(null);
setSelectedEventTypeLabel(null);
};
const handleEventTypeSelect = (eventTypeId: number | null, label?: string | null) => {
if (eventTypeId === null) {
clearEventTypeFilter();
} else {
setSelectedEventTypeId(eventTypeId);
setSelectedEventTypeLabel(label || null);
}
};
const renderFilterControls = () => {
const filterLabel =
selectedEventTypeId !== null ? selectedEventTypeLabel || "Event Type" : "Filter";
return (
<View
className="border-b border-gray-300 bg-gray-100 px-2 py-2 md:px-4"
style={{
borderBottomColor: theme.borderSubtle,
backgroundColor: isDark ? theme.background : undefined,
}}
>
<View className="flex-row items-center gap-3">
{/* Dropdown menu for event type filter */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<AppPressable
className="flex-row items-center rounded-lg border border-gray-200 bg-white"
style={{
borderColor: theme.border,
backgroundColor: theme.backgroundSecondary,
paddingHorizontal: 8,
paddingVertical: 6,
flexDirection: "row",
alignItems: "center",
}}
>
<Ionicons name="options-outline" size={14} color={theme.textSecondary} />
<Text
className={`text-sm ${selectedEventTypeId !== null ? "font-semibold" : ""}`}
style={{
marginLeft: 4,
color: selectedEventTypeId !== null ? theme.text : theme.textSecondary,
}}
numberOfLines={1}
>
{filterLabel}
</Text>
<Ionicons
name="chevron-down"
size={12}
color={theme.textSecondary}
style={{ marginLeft: 2 }}
/>
</AppPressable>
</DropdownMenuTrigger>
<DropdownMenuContent
insets={{ top: 60, bottom: 20, left: 12, right: 12 }}
sideOffset={8}
className="w-52"
align="start"
>
{/* Clear filter option */}
<DropdownMenuCheckboxItem
checked={selectedEventTypeId === null}
onCheckedChange={() => handleEventTypeSelect(null)}
>
<Text className="text-base" style={{ color: theme.text }}>
All Event Types
</Text>
</DropdownMenuCheckboxItem>
{/* Event type options */}
{eventTypes.map((eventType) => (
<DropdownMenuCheckboxItem
key={eventType.id}
checked={selectedEventTypeId === eventType.id}
onCheckedChange={() => handleEventTypeSelect(eventType.id, eventType.title)}
>
<Text className="text-base" style={{ color: theme.text }} numberOfLines={1}>
{eventType.title}
</Text>
</DropdownMenuCheckboxItem>
))}
{/* Loading state */}
{eventTypesLoading && eventTypes.length === 0 && (
<DropdownMenuCheckboxItem checked={false} onCheckedChange={() => {}}>
<Text className="text-base text-gray-500 dark:text-gray-400">Loading...</Text>
</DropdownMenuCheckboxItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<View style={{ flex: 1 }}>
<TextInput
className="rounded-lg border border-gray-200 bg-white px-3 py-2 text-sm text-black"
style={{
borderColor: theme.border,
backgroundColor: theme.backgroundSecondary,
color: theme.text,
}}
placeholder="Search bookings"
placeholderTextColor={theme.textSecondary}
value={searchQuery}
onChangeText={handleSearch}
autoCapitalize="none"
autoCorrect={false}
clearButtonMode="while-editing"
/>
</View>
</View>
</View>
);
};
return (
<BookingListScreen
renderHeader={() => (
<Header
filterOptions={filterOptions}
activeFilter={activeFilter}
onFilterChange={handleFilterChange as (filterKey: string) => void}
/>
)}
renderFilterControls={renderFilterControls}
eventTypes={eventTypes}
eventTypesLoading={eventTypesLoading}
searchQuery={searchQuery}
selectedEventTypeId={selectedEventTypeId}
onEventTypeChange={handleEventTypeSelect}
activeFilter={activeFilter}
filterParams={filterParams}
/>
);
}