Skip to content

Commit c1e5e0a

Browse files
authored
feat(companion): new availability detail and actions pages for ios (calcom#26424)
* version 1 * version 1.1 * better code * covered all edge cases * address cubics comments * address cubics comments
1 parent f9dc3f2 commit c1e5e0a

38 files changed

Lines changed: 4548 additions & 71 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { Stack } from "expo-router";
2+
import { Platform } from "react-native";
23

34
export default function AvailabilityLayout() {
45
return (
56
<Stack>
67
<Stack.Screen name="index" options={{ headerShown: false }} />
8+
<Stack.Screen name="availability-detail" options={{ headerShown: Platform.OS === "ios" }} />
79
</Stack>
810
);
911
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
2+
import { useCallback, useRef } from "react";
3+
import {
4+
AvailabilityDetailScreen,
5+
type AvailabilityDetailScreenHandle,
6+
} from "@/components/screens/AvailabilityDetailScreen";
7+
8+
// Type for action handlers exposed by AvailabilityDetailScreen.ios.tsx
9+
type ActionHandlers = {
10+
handleSetAsDefault: () => void;
11+
handleDelete: () => void;
12+
};
13+
14+
export default function AvailabilityDetailIOS() {
15+
const { id } = useLocalSearchParams<{ id: string }>();
16+
const router = useRouter();
17+
18+
// Ref to store action handlers from AvailabilityDetailScreen
19+
const actionHandlersRef = useRef<ActionHandlers | null>(null);
20+
const screenRef = useRef<AvailabilityDetailScreenHandle>(null);
21+
22+
// Callback to receive action handlers from AvailabilityDetailScreen
23+
const handleActionsReady = useCallback((handlers: ActionHandlers) => {
24+
actionHandlersRef.current = handlers;
25+
}, []);
26+
27+
// Navigation handlers for edit bottom sheets
28+
const handleEditNameAndTimezone = useCallback(() => {
29+
router.push(`/edit-availability-name?id=${id}` as never);
30+
}, [router, id]);
31+
32+
const handleEditWorkingHours = useCallback(() => {
33+
router.push(`/edit-availability-hours?id=${id}` as never);
34+
}, [router, id]);
35+
36+
const handleEditOverride = useCallback(() => {
37+
router.push(`/edit-availability-override?id=${id}` as never);
38+
}, [router, id]);
39+
40+
// Action handlers for inline actions
41+
const handleSetAsDefault = useCallback(() => {
42+
if (actionHandlersRef.current?.handleSetAsDefault) {
43+
actionHandlersRef.current.handleSetAsDefault();
44+
} else if (screenRef.current?.setAsDefault) {
45+
screenRef.current.setAsDefault();
46+
}
47+
}, []);
48+
49+
const handleDelete = useCallback(() => {
50+
if (actionHandlersRef.current?.handleDelete) {
51+
actionHandlersRef.current.handleDelete();
52+
} else if (screenRef.current?.delete) {
53+
screenRef.current.delete();
54+
}
55+
}, []);
56+
57+
if (!id) {
58+
return null;
59+
}
60+
61+
return (
62+
<>
63+
<Stack.Screen
64+
options={{
65+
title: "Availability",
66+
headerBackTitle: "Availability",
67+
headerBackButtonDisplayMode: "default",
68+
headerTitle: "",
69+
headerStyle: {
70+
backgroundColor: "#f2f2f7",
71+
},
72+
headerShadowVisible: false,
73+
}}
74+
/>
75+
76+
<Stack.Header style={{ shadowColor: "transparent", backgroundColor: "#f2f2f7" }}>
77+
<Stack.Header.Right>
78+
{/* Edit Menu */}
79+
<Stack.Header.Menu>
80+
<Stack.Header.Label>Edit</Stack.Header.Label>
81+
82+
{/* Name and Timezone */}
83+
<Stack.Header.MenuAction icon="pencil" onPress={handleEditNameAndTimezone}>
84+
Name and Timezone
85+
</Stack.Header.MenuAction>
86+
87+
{/* Working Hours */}
88+
<Stack.Header.MenuAction icon="clock" onPress={handleEditWorkingHours}>
89+
Working Hours
90+
</Stack.Header.MenuAction>
91+
92+
{/* Date Override */}
93+
<Stack.Header.MenuAction icon="calendar.badge.plus" onPress={handleEditOverride}>
94+
Date Override
95+
</Stack.Header.MenuAction>
96+
97+
{/* Set as Default */}
98+
<Stack.Header.MenuAction icon="star" onPress={handleSetAsDefault}>
99+
Set as Default
100+
</Stack.Header.MenuAction>
101+
102+
{/* Delete */}
103+
<Stack.Header.MenuAction icon="trash" onPress={handleDelete} destructive>
104+
Delete Schedule
105+
</Stack.Header.MenuAction>
106+
</Stack.Header.Menu>
107+
</Stack.Header.Right>
108+
</Stack.Header>
109+
110+
<AvailabilityDetailScreen
111+
ref={screenRef}
112+
id={id}
113+
// @ts-expect-error - onActionsReady is only available in AvailabilityDetailScreen.ios.tsx
114+
onActionsReady={handleActionsReady}
115+
/>
116+
</>
117+
);
118+
}

companion/app/availability-detail.tsx renamed to companion/app/(tabs)/(availability)/availability-detail.tsx

File renamed without changes.

companion/app/(tabs)/(availability)/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export default function Availability() {
6060
onSuccess: (newSchedule) => {
6161
// Navigate to edit the newly created schedule
6262
router.push({
63-
pathname: "/availability-detail",
63+
pathname: "/(tabs)/(availability)/availability-detail",
6464
params: {
6565
id: newSchedule.id.toString(),
6666
},

companion/app/(tabs)/(event-types)/event-type-detail.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
TouchableOpacity,
1414
View,
1515
} from "react-native";
16-
import { useSafeAreaInsets } from "react-native-safe-area-context";
1716
import { AppPressable } from "@/components/AppPressable";
1817
import { AdvancedTab } from "@/components/event-type-detail/tabs/AdvancedTab";
1918
import { AvailabilityTab } from "@/components/event-type-detail/tabs/AvailabilityTab";
@@ -127,7 +126,6 @@ export default function EventTypeDetail() {
127126
slug?: string;
128127
}>();
129128

130-
const insets = useSafeAreaInsets();
131129
const [activeTab, setActiveTab] = useState("basics");
132130

133131
// Form state

companion/app/_layout.tsx

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,110 @@ function RootLayoutContent() {
198198
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
199199
}}
200200
/>
201+
<Stack.Screen
202+
name="edit-availability-name"
203+
options={{
204+
headerShown: true,
205+
headerTransparent: Platform.OS === "ios",
206+
headerLargeTitle: false,
207+
title: "Edit Name & Timezone",
208+
presentation:
209+
Platform.OS === "ios"
210+
? isLiquidGlassAvailable()
211+
? "formSheet"
212+
: "modal"
213+
: "containedModal",
214+
sheetGrabberVisible: Platform.OS === "ios",
215+
sheetAllowedDetents: Platform.OS === "ios" ? [0.5, 0.7] : undefined,
216+
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
217+
contentStyle: {
218+
backgroundColor:
219+
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
220+
},
221+
headerStyle: {
222+
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
223+
},
224+
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
225+
}}
226+
/>
227+
<Stack.Screen
228+
name="edit-availability-hours"
229+
options={{
230+
headerShown: true,
231+
headerTransparent: Platform.OS === "ios",
232+
headerLargeTitle: false,
233+
title: "Working Hours",
234+
presentation:
235+
Platform.OS === "ios"
236+
? isLiquidGlassAvailable()
237+
? "formSheet"
238+
: "modal"
239+
: "containedModal",
240+
sheetGrabberVisible: Platform.OS === "ios",
241+
sheetAllowedDetents: Platform.OS === "ios" ? [0.7, 1] : undefined,
242+
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
243+
contentStyle: {
244+
backgroundColor:
245+
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
246+
},
247+
headerStyle: {
248+
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
249+
},
250+
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
251+
}}
252+
/>
253+
<Stack.Screen
254+
name="edit-availability-day"
255+
options={{
256+
headerShown: true,
257+
headerTransparent: Platform.OS === "ios",
258+
headerLargeTitle: false,
259+
title: "Edit Day",
260+
presentation:
261+
Platform.OS === "ios"
262+
? isLiquidGlassAvailable()
263+
? "formSheet"
264+
: "modal"
265+
: "containedModal",
266+
sheetGrabberVisible: Platform.OS === "ios",
267+
sheetAllowedDetents: Platform.OS === "ios" ? [0.6, 0.9] : undefined,
268+
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
269+
contentStyle: {
270+
backgroundColor:
271+
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
272+
},
273+
headerStyle: {
274+
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
275+
},
276+
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
277+
}}
278+
/>
279+
<Stack.Screen
280+
name="edit-availability-override"
281+
options={{
282+
headerShown: true,
283+
headerTransparent: Platform.OS === "ios",
284+
headerLargeTitle: false,
285+
title: "Date Override",
286+
presentation:
287+
Platform.OS === "ios"
288+
? isLiquidGlassAvailable()
289+
? "formSheet"
290+
: "modal"
291+
: "containedModal",
292+
sheetGrabberVisible: Platform.OS === "ios",
293+
sheetAllowedDetents: Platform.OS === "ios" ? [0.7, 0.9] : undefined,
294+
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
295+
contentStyle: {
296+
backgroundColor:
297+
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
298+
},
299+
headerStyle: {
300+
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
301+
},
302+
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
303+
}}
304+
/>
201305
</Stack>
202306
) : (
203307
<LoginScreenComponent />
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { osName } from "expo-device";
2+
import { isLiquidGlassAvailable } from "expo-glass-effect";
3+
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
4+
import { useCallback, useEffect, useRef, useState } from "react";
5+
import { ActivityIndicator, Alert, View } from "react-native";
6+
import { useSafeAreaInsets } from "react-native-safe-area-context";
7+
import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen.ios";
8+
import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen.ios";
9+
import { CalComAPIService, type Schedule } from "@/services/calcom";
10+
11+
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
12+
13+
// Semi-transparent background to prevent black flash while preserving glass effect
14+
const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
15+
16+
function getPresentationStyle(): "formSheet" | "modal" {
17+
if (isLiquidGlassAvailable() && osName !== "iPadOS") {
18+
return "formSheet";
19+
}
20+
return "modal";
21+
}
22+
23+
export default function EditAvailabilityDayIOS() {
24+
const { id, day } = useLocalSearchParams<{ id: string; day: string }>();
25+
const router = useRouter();
26+
const insets = useSafeAreaInsets();
27+
const [schedule, setSchedule] = useState<Schedule | null>(null);
28+
const [isLoading, setIsLoading] = useState(true);
29+
const [isSaving, setIsSaving] = useState(false);
30+
31+
const screenRef = useRef<EditAvailabilityDayScreenHandle>(null);
32+
33+
const dayIndex = day ? parseInt(day, 10) : 0;
34+
const dayName = DAYS[dayIndex] || "Day";
35+
36+
useEffect(() => {
37+
if (id) {
38+
setIsLoading(true);
39+
CalComAPIService.getScheduleById(Number(id))
40+
.then(setSchedule)
41+
.catch(() => {
42+
Alert.alert("Error", "Failed to load schedule details");
43+
router.back();
44+
})
45+
.finally(() => setIsLoading(false));
46+
} else {
47+
setIsLoading(false);
48+
Alert.alert("Error", "Schedule ID is missing");
49+
router.back();
50+
}
51+
}, [id, router]);
52+
53+
const handleSave = useCallback(() => {
54+
screenRef.current?.submit();
55+
}, []);
56+
57+
const handleSuccess = useCallback(() => {
58+
router.back();
59+
}, [router]);
60+
61+
const presentationStyle = getPresentationStyle();
62+
const useGlassEffect = isLiquidGlassAvailable();
63+
64+
return (
65+
<>
66+
<Stack.Screen
67+
options={{
68+
title: dayName,
69+
presentation: presentationStyle,
70+
sheetGrabberVisible: true,
71+
sheetAllowedDetents: [0.6, 0.9],
72+
sheetInitialDetentIndex: 0,
73+
contentStyle: {
74+
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
75+
},
76+
}}
77+
/>
78+
79+
<Stack.Header>
80+
<Stack.Header.Left>
81+
<Stack.Header.Button onPress={() => router.back()}>
82+
<Stack.Header.Icon sf="xmark" />
83+
</Stack.Header.Button>
84+
</Stack.Header.Left>
85+
86+
<Stack.Header.Title>{dayName}</Stack.Header.Title>
87+
88+
<Stack.Header.Right>
89+
<Stack.Header.Button
90+
onPress={handleSave}
91+
disabled={isSaving}
92+
variant="prominent"
93+
tintColor="#000"
94+
>
95+
<Stack.Header.Icon sf="checkmark" />
96+
</Stack.Header.Button>
97+
</Stack.Header.Right>
98+
</Stack.Header>
99+
100+
<View
101+
style={{
102+
flex: 1,
103+
paddingTop: 56,
104+
paddingBottom: insets.bottom,
105+
}}
106+
>
107+
{isLoading ? (
108+
<View className="flex-1 items-center justify-center">
109+
<ActivityIndicator size="large" color="#007AFF" />
110+
</View>
111+
) : (
112+
<EditAvailabilityDayScreenComponent
113+
ref={screenRef}
114+
schedule={schedule}
115+
dayIndex={dayIndex}
116+
onSuccess={handleSuccess}
117+
onSavingChange={setIsSaving}
118+
transparentBackground={useGlassEffect}
119+
/>
120+
)}
121+
</View>
122+
</>
123+
);
124+
}

0 commit comments

Comments
 (0)