feat(companion): availability detail page for android and extension (#26560)

* Availability Detail Page Header and fix event type payment amount badge, and index ui bug bottom navbar

* feat(companion): implement read-only availability detail page for Android/Extension

- Convert AvailabilityDetailScreen.tsx to read-only display with expandable sections
- Add header dropdown menu with edit options in availability-detail.tsx
- Update edit-availability-name.tsx with modal presentation pattern
- Update edit-availability-hours.tsx with modal presentation pattern
- Update edit-availability-override.tsx with modal presentation pattern
- Match iOS flow: read-only detail page with edit actions via header menu

* fix header on Availability Detail screen for android

* make dialoges center on extension

* update padding

* AvailabilityDetailScreen for android and extension

* added click to modal feat

* fix Date Overrides Card was always shown

---------

Co-authored-by: Volnei Munhoz <volnei@cal.com>
This commit is contained in:
Dhairyashil ShindeandVolnei Munhoz authored and GitHub committed 2026-01-08 23:31:59 +05:30
1 parent a748a75b08
commit 84d1ce5bff
17 files changed
+941 -1210

No files matched your search

@@ -5,7 +5,7 @@ export default function AvailabilityLayout() {
return (
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="availability-detail" options={{ headerShown: Platform.OS === "ios" }} />
<Stack.Screen name="availability-detail" />
</Stack>
);
}
@@ -1,47 +1,127 @@
import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useRef } from "react";
import { Platform } from "react-native";
import { useCallback, useRef } from "react";
import { Text, TouchableOpacity } from "react-native";
import {
AvailabilityDetailScreen,
type AvailabilityDetailScreenHandle,
} from "@/components/screens/AvailabilityDetailScreen";
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
// Type for action handlers exposed by AvailabilityDetailScreen
type ActionHandlers = {
handleSetAsDefault: () => void;
handleDelete: () => void;
};
export default function AvailabilityDetail() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
// Ref to store action handlers from AvailabilityDetailScreen
const actionHandlersRef = useRef<ActionHandlers | null>(null);
const screenRef = useRef<AvailabilityDetailScreenHandle>(null);
// Callback to receive action handlers from AvailabilityDetailScreen
const handleActionsReady = useCallback((handlers: ActionHandlers) => {
actionHandlersRef.current = handlers;
}, []);
// Navigation handlers for edit modal screens
const handleEditNameAndTimezone = useCallback(() => {
router.push(`/edit-availability-name?id=${id}` as never);
}, [router, id]);
const handleEditWorkingHours = useCallback(() => {
router.push(`/edit-availability-hours?id=${id}` as never);
}, [router, id]);
const handleEditOverride = useCallback(() => {
router.push(`/edit-availability-override?id=${id}` as never);
}, [router, id]);
// Action handlers for inline actions
const handleSetAsDefault = useCallback(() => {
if (actionHandlersRef.current?.handleSetAsDefault) {
actionHandlersRef.current.handleSetAsDefault();
} else if (screenRef.current?.setAsDefault) {
screenRef.current.setAsDefault();
}
}, []);
const handleDelete = useCallback(() => {
if (actionHandlersRef.current?.handleDelete) {
actionHandlersRef.current.handleDelete();
} else if (screenRef.current?.delete) {
screenRef.current.delete();
}
}, []);
if (!id) {
return null;
}
const handleSave = () => {
screenRef.current?.save();
};
return (
<>
<Stack.Screen options={{ headerShown: false }} />
<Stack.Screen
options={{
title: "Availability",
headerBackTitle: "Availability",
headerStyle: {
backgroundColor: "#f2f2f7",
},
headerShadowVisible: false,
headerRight: () => (
<HeaderButtonWrapper side="right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<TouchableOpacity
style={{
padding: 8,
marginRight: -8,
}}
>
<Ionicons name="ellipsis-horizontal-circle" size={24} color="#007AFF" />
</TouchableOpacity>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
<DropdownMenuItem onPress={handleEditNameAndTimezone}>
<Ionicons name="pencil-outline" size={18} color="#000" />
<Text className="ml-2 text-[15px] text-black">Name and Timezone</Text>
</DropdownMenuItem>
<DropdownMenuItem onPress={handleEditWorkingHours}>
<Ionicons name="time-outline" size={18} color="#000" />
<Text className="ml-2 text-[15px] text-black">Working Hours</Text>
</DropdownMenuItem>
<DropdownMenuItem onPress={handleEditOverride}>
<Ionicons name="calendar-outline" size={18} color="#000" />
<Text className="ml-2 text-[15px] text-black">Date Override</Text>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onPress={handleSetAsDefault}>
<Ionicons name="star-outline" size={18} color="#000" />
<Text className="ml-2 text-[15px] text-black">Set as Default</Text>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onPress={handleDelete} variant="destructive">
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
<Text className="ml-2 text-[15px] text-[#FF3B30]">Delete Schedule</Text>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</HeaderButtonWrapper>
),
}}
/>
{Platform.OS === "ios" && (
<Stack.Header>
<Stack.Header.Left>
<Stack.Header.Button onPress={() => router.back()}>
<Stack.Header.Icon sf="chevron.left" />
</Stack.Header.Button>
</Stack.Header.Left>
<Stack.Header.Title>Edit Availability</Stack.Header.Title>
<Stack.Header.Right>
<Stack.Header.Button onPress={handleSave} variant="prominent" tintColor="#000">
Save
</Stack.Header.Button>
</Stack.Header.Right>
</Stack.Header>
)}
<AvailabilityDetailScreen ref={screenRef} id={id} useNativeHeader={Platform.OS === "ios"} />
<AvailabilityDetailScreen ref={screenRef} id={id} onActionsReady={handleActionsReady} />
</>
);
}
@@ -1256,46 +1256,6 @@ export default function EventTypeDetail() {
contentContainerStyle={{ padding: 16, paddingBottom: 200 }}
contentInsetAdjustmentBehavior="automatic"
>
{/* Horizontal tabs only shown on web; Android uses header dropdown menu */}
<Activity mode={Platform.OS === "web" ? "visible" : "hidden"}>
<View
style={{
paddingBottom: 12,
}}
>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 12, gap: 4 }}
>
{tabs.map((tab) => (
<TouchableOpacity
key={tab.id}
className={`min-w-[90px] items-center rounded-[24px] px-3 py-3 md:px-5 ${
activeTab === tab.id ? "bg-[#EEEFF2]" : ""
}`}
onPress={() => setActiveTab(tab.id)}
>
<View className="flex-row items-center gap-2">
<Ionicons
name={tab.icon}
size={18}
color={activeTab === tab.id ? "#007AFF" : "#666"}
/>
<Text
className={`text-base font-medium ${
activeTab === tab.id ? "font-semibold text-[#007AFF]" : "text-[#666]"
}`}
>
{tab.label}
</Text>
</View>
</TouchableOpacity>
))}
</ScrollView>
</View>
</Activity>
{activeTab === "basics" ? (
<BasicsTab
eventTitle={eventTitle}
+6
View File
@@ -70,6 +70,12 @@ function WebTabs() {
},
}}
>
<Tabs.Screen
name="index"
options={{
href: null,
}}
/>
<Tabs.Screen
name="(event-types)"
options={{
+32 -3
View File
@@ -1,7 +1,9 @@
import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { ActivityIndicator, Alert, View } from "react-native";
import { ActivityIndicator, Alert, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen";
import { CalComAPIService, type Schedule } from "@/services/calcom";
@@ -29,6 +31,10 @@ export default function EditAvailabilityHours() {
}
}, [id, router]);
const handleClose = useCallback(() => {
router.back();
}, [router]);
const handleDayPress = useCallback(
(dayIndex: number) => {
router.push(`/edit-availability-day?id=${id}&day=${dayIndex}` as never);
@@ -42,7 +48,12 @@ export default function EditAvailabilityHours() {
className="flex-1 items-center justify-center bg-white"
style={{ paddingBottom: insets.bottom }}
>
<Stack.Screen options={{ title: "Working Hours" }} />
<Stack.Screen
options={{
title: "Working Hours",
presentation: "modal",
}}
/>
<ActivityIndicator size="large" color="#007AFF" />
</View>
);
@@ -50,7 +61,25 @@ export default function EditAvailabilityHours() {
return (
<View className="flex-1 bg-white" style={{ paddingBottom: insets.bottom }}>
<Stack.Screen options={{ title: "Working Hours" }} />
<Stack.Screen
options={{
title: "Working Hours",
presentation: "modal",
contentStyle: {
backgroundColor: "#FFFFFF",
},
headerStyle: {
backgroundColor: "#FFFFFF",
},
headerLeft: () => (
<HeaderButtonWrapper side="left">
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
<Ionicons name="close" size={24} color="#000" />
</TouchableOpacity>
</HeaderButtonWrapper>
),
}}
/>
<EditAvailabilityHoursScreenComponent schedule={schedule} onDayPress={handleDayPress} />
</View>
);
+26 -2
View File
@@ -1,6 +1,7 @@
import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Alert, Text, View } from "react-native";
import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen";
@@ -34,6 +35,10 @@ export default function EditAvailabilityName() {
}
}, [id, router]);
const handleClose = useCallback(() => {
router.back();
}, [router]);
const handleSave = useCallback(() => {
if (isSaving) return;
screenRef.current?.submit();
@@ -49,7 +54,12 @@ export default function EditAvailabilityName() {
className="flex-1 items-center justify-center bg-white"
style={{ paddingBottom: insets.bottom }}
>
<Stack.Screen options={{ title: "Edit Name & Timezone" }} />
<Stack.Screen
options={{
title: "Edit Name & Timezone",
presentation: "modal",
}}
/>
<ActivityIndicator size="large" color="#007AFF" />
</View>
);
@@ -60,6 +70,20 @@ export default function EditAvailabilityName() {
<Stack.Screen
options={{
title: "Edit Name & Timezone",
presentation: "modal",
contentStyle: {
backgroundColor: "#FFFFFF",
},
headerStyle: {
backgroundColor: "#FFFFFF",
},
headerLeft: () => (
<HeaderButtonWrapper side="left">
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
<Ionicons name="close" size={24} color="#000" />
</TouchableOpacity>
</HeaderButtonWrapper>
),
headerRight: () => (
<HeaderButtonWrapper side="right">
<Text
+26 -2
View File
@@ -1,6 +1,7 @@
import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import { useCallback, useEffect, useRef, useState } from "react";
import { ActivityIndicator, Alert, Text, View } from "react-native";
import { ActivityIndicator, Alert, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen";
@@ -40,6 +41,10 @@ export default function EditAvailabilityOverride() {
}
}, [id, router]);
const handleClose = useCallback(() => {
router.back();
}, [router]);
const handleSave = useCallback(() => {
if (isSaving) return;
screenRef.current?.submit();
@@ -66,7 +71,12 @@ export default function EditAvailabilityOverride() {
className="flex-1 items-center justify-center bg-white"
style={{ paddingBottom: insets.bottom }}
>
<Stack.Screen options={{ title }} />
<Stack.Screen
options={{
title,
presentation: "modal",
}}
/>
<ActivityIndicator size="large" color="#007AFF" />
</View>
);
@@ -77,6 +87,20 @@ export default function EditAvailabilityOverride() {
<Stack.Screen
options={{
title,
presentation: "modal",
contentStyle: {
backgroundColor: "#FFFFFF",
},
headerStyle: {
backgroundColor: "#FFFFFF",
},
headerLeft: () => (
<HeaderButtonWrapper side="left">
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
<Ionicons name="close" size={24} color="#000" />
</TouchableOpacity>
</HeaderButtonWrapper>
),
headerRight: () => (
<HeaderButtonWrapper side="right">
<Text
@@ -134,7 +134,15 @@ export function EventTypeBadges({
{/* Price Badge */}
{hasPrice && formattedPrice ? (
<Text className="text-sm font-medium text-cal-accent-success">{formattedPrice}</Text>
<View
className="rounded-md border border-cal-border bg-cal-border"
style={{ height: 24, paddingHorizontal: 8, flexDirection: "row", alignItems: "center" }}
>
<Ionicons name="card-outline" size={14} color="#000000" />
<Text className="ml-1.5 text-xs font-semibold text-cal-brand-black">
{formattedPrice}
</Text>
</View>
) : null}
{/* Repeats Badge */}
@@ -20,7 +20,21 @@ export function useEventTypeListItemData(item: EventType): EventTypeListItemData
const formattedDuration = formatDuration(duration);
const normalizedDescription = item.description ? normalizeMarkdown(item.description) : null;
const hasPrice = item.price != null && item.price > 0;
const formattedPrice = hasPrice ? `${item.currency || "$"}${item.price}` : null;
let formattedPrice: string | null = null;
if (hasPrice && item.price) {
// Price is in cents, convert to main unit
const priceAmount = item.price / 100;
try {
formattedPrice = new Intl.NumberFormat("en-US", {
style: "currency",
currency: item.currency?.toUpperCase() || "USD",
}).format(priceAmount);
} catch {
// Fallback if formatting fails
formattedPrice = `${item.currency?.toUpperCase() || "$"}${priceAmount.toFixed(2)}`;
}
}
return {
duration,
@@ -114,8 +114,6 @@ export const AvailabilityDetailScreen = forwardRef<
endTime: string;
}[]
>([]);
const [daysExpanded, setDaysExpanded] = useState(true);
const [overridesExpanded, setOverridesExpanded] = useState(true);
const processScheduleData = useCallback(
(scheduleData: NonNullable<Awaited<ReturnType<typeof CalComAPIService.getScheduleById>>>) => {
@@ -327,46 +325,20 @@ export const AvailabilityDetailScreen = forwardRef<
</View>
</View>
{/* Working Hours Summary Card */}
{/* Weekly Schedule Card - Navigable */}
<View className="mb-4 overflow-hidden rounded-xl bg-white">
<View className="px-4 py-3.5">
<Text className="mb-2.5 text-[13px] font-medium uppercase tracking-wide text-[#8E8E93]">
Working Hours
</Text>
{Object.keys(availability).length > 0 ? (
<View>
{formatAvailabilityDisplay(availability).map((line) => (
<Text key={line} className="mb-1 text-[17px] text-black">
{line}
</Text>
))}
<AppPressable onPress={() => router.push(`/edit-availability-hours?id=${id}` as never)}>
<View className="flex-row items-center justify-between px-4 py-3.5">
<Text className="text-[17px] text-black">Weekly Schedule</Text>
<View className="flex-row items-center">
<Text className="mr-1 text-[17px] text-[#8E8E93]">
{enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"}
</Text>
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
</View>
) : (
<Text className="text-[17px] text-[#8E8E93]">No availability set</Text>
)}
</View>
</View>
{/* Weekly Schedule Card - Expandable */}
<View className="mb-4 overflow-hidden rounded-xl bg-white">
<AppPressable
className="flex-row items-center justify-between px-4 py-3.5"
onPress={() => setDaysExpanded(!daysExpanded)}
>
<Text className="text-[17px] text-black">Weekly Schedule</Text>
<View className="flex-row items-center">
<Text className="mr-1 text-[17px] text-[#8E8E93]">
{enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"}
</Text>
<Ionicons
name={daysExpanded ? "chevron-down" : "chevron-forward"}
size={18}
color="#C7C7CC"
/>
</View>
</AppPressable>
{daysExpanded && (
{/* Expanded Day List */}
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
{DAYS.map((day, dayIndex) => {
const daySlots = availability[dayIndex] || [];
@@ -412,38 +384,39 @@ export const AvailabilityDetailScreen = forwardRef<
);
})}
</View>
)}
</AppPressable>
</View>
{/* Timezone Card */}
{/* Timezone Card - Navigable */}
<View className="mb-4 overflow-hidden rounded-xl bg-white">
<View className="flex-row items-center justify-between px-4 py-3.5">
<Text className="text-[17px] text-black">Timezone</Text>
<Text className="text-[17px] text-[#8E8E93]">{timeZone}</Text>
</View>
<AppPressable onPress={() => router.push(`/edit-availability-name?id=${id}` as never)}>
<View className="flex-row items-center justify-between px-4 py-3.5">
<Text className="text-[17px] text-black">Timezone</Text>
<View className="flex-row items-center">
<Text className="mr-1 text-[17px] text-[#8E8E93]">{timeZone}</Text>
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
</View>
</View>
</AppPressable>
</View>
{/* Date Overrides Card - Expandable */}
{/* Date Overrides Card - Navigable */}
{overrides.length > 0 && (
<View className="mb-4 overflow-hidden rounded-xl bg-white">
<AppPressable
className="flex-row items-center justify-between px-4 py-3.5"
onPress={() => setOverridesExpanded(!overridesExpanded)}
onPress={() => router.push(`/edit-availability-override?id=${id}` as never)}
>
<Text className="text-[17px] text-black">Date Overrides</Text>
<View className="flex-row items-center">
<Text className="mr-1 text-[17px] text-[#8E8E93]">
{overrides.length} {overrides.length === 1 ? "override" : "overrides"}
</Text>
<Ionicons
name={overridesExpanded ? "chevron-down" : "chevron-forward"}
size={18}
color="#C7C7CC"
/>
<View className="flex-row items-center justify-between px-4 py-3.5">
<Text className="text-[17px] text-black">Date Overrides</Text>
<View className="flex-row items-center">
<Text className="mr-1 text-[17px] text-[#8E8E93]">
{overrides.length} {overrides.length === 1 ? "override" : "overrides"}
</Text>
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
</View>
</View>
</AppPressable>
{overridesExpanded && (
{/* Expanded Overrides List */}
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
{overrides.map((override, index) => (
<View
@@ -464,7 +437,7 @@ export const AvailabilityDetailScreen = forwardRef<
</View>
))}
</View>
)}
</AppPressable>
</View>
)}
File diff suppressed because it is too large. Load diff
@@ -9,6 +9,7 @@ import {
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { AppPressable } from "@/components/AppPressable";
@@ -290,13 +291,13 @@ export function AvailabilityListScreen({
autoCorrect={false}
clearButtonMode="while-editing"
/>
<AppPressable
<TouchableOpacity
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
onPress={handleCreateNew}
>
<Ionicons name="add" size={18} color="#fff" />
<Text className="text-base font-semibold text-white">New</Text>
</AppPressable>
</TouchableOpacity>
</View>
</Activity>
@@ -439,12 +440,12 @@ export function AvailabilityListScreen({
animationType="fade"
onRequestClose={() => onShowCreateModalChange(false)}
>
<AppPressable
<TouchableOpacity
className="flex-1 items-center justify-center bg-black/50 p-2 md:p-4"
activeOpacity={1}
onPress={() => onShowCreateModalChange(false)}
>
<AppPressable
<TouchableOpacity
className="w-[90%] max-w-[500px] rounded-2xl bg-white"
activeOpacity={1}
onPress={(e) => e.stopPropagation()}
@@ -452,7 +453,10 @@ export function AvailabilityListScreen({
>
{/* Header */}
<View className="px-8 pb-4 pt-6">
<Text className="text-2xl font-semibold text-gray-900">Add a new schedule</Text>
<Text className="mb-2 text-2xl font-semibold text-gray-900">
Add a new schedule
</Text>
<Text className="text-sm text-gray-500">Create a new availability schedule.</Text>
</View>
{/* Content */}
@@ -497,8 +501,8 @@ export function AvailabilityListScreen({
</AppPressable>
</View>
</View>
</AppPressable>
</AppPressable>
</TouchableOpacity>
</TouchableOpacity>
</FullScreenModal>
)}
@@ -599,7 +599,7 @@ export function BookingDetailScreen({
}}
/>
)}
<View className="flex-1 bg-[#]">
<View className="flex-1 bg-[#f8f9fa]">
<ScrollView
className="flex-1"
contentContainerStyle={{ padding: 16, paddingBottom: 100 }}
@@ -1,6 +1,6 @@
import { Ionicons } from "@expo/vector-icons";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import { Alert, ScrollView, Switch, Text, View } from "react-native";
import { Alert, Platform, ScrollView, Switch, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
import { FullScreenModal } from "@/components/FullScreenModal";
@@ -8,6 +8,7 @@ import type { Schedule } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import type { ScheduleAvailability } from "@/services/types";
import { showErrorAlert } from "@/utils/alerts";
import { shadows } from "@/utils/shadows";
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
@@ -179,6 +180,57 @@ export const EditAvailabilityDayScreen = forwardRef<
const dayName = DAYS[dayIndex] || "Day";
// Render modal content
const renderTimePickerContent = () => (
<>
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-4">
<Text className="text-[17px] font-semibold">
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
</Text>
<AppPressable onPress={() => setShowTimePicker(null)}>
<Ionicons name="close" size={24} color="#8E8E93" />
</AppPressable>
</View>
<ScrollView className="px-4 py-3">
{TIME_OPTIONS.map((time) => {
const slotIndex = showTimePicker?.slotIndex ?? 0;
const currentTime =
showTimePicker?.type === "start"
? slots[slotIndex]?.startTime
: slots[slotIndex]?.endTime;
const isSelected = currentTime === time;
return (
<AppPressable key={time} onPress={() => handleTimeSelect(time)}>
<View
className={`mb-2.5 rounded-xl border-2 px-4 py-4 ${
isSelected
? "border-[#007AFF] bg-blue-50 shadow-md"
: "border-gray-200 bg-gray-50"
}`}
>
<View className="flex-row items-center justify-between">
<Text
className={`text-[17px] ${
isSelected ? "font-semibold text-[#007AFF]" : "font-medium text-gray-900"
}`}
>
{formatTime12Hour(time)}
</Text>
{isSelected && (
<View className="rounded-full bg-[#007AFF] p-1.5">
<Ionicons name="checkmark" size={14} color="#FFFFFF" />
</View>
)}
</View>
</View>
</AppPressable>
);
})}
</ScrollView>
</>
);
// Initialize from schedule
useEffect(() => {
if (schedule) {
@@ -308,7 +360,7 @@ export const EditAvailabilityDayScreen = forwardRef<
}}
>
{/* Enable/Disable Toggle */}
<View className="mb-4 flex-row items-center justify-between rounded-lg border border-gray-200 px-4 py-3.5">
<View className="mb-4 flex-row items-center justify-between rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3.5">
<Text className="text-[17px] font-medium text-black">Available on {dayName}</Text>
<Switch
value={isEnabled}
@@ -326,59 +378,117 @@ export const EditAvailabilityDayScreen = forwardRef<
{slots.map((slot, index) => (
<View
key={`slot-${slot.startTime}-${slot.endTime}-${index}`}
className="mb-3 rounded-lg border border-gray-200 px-4 py-3"
className="mb-3 rounded-xl border-2 border-gray-200 bg-white px-4 py-4"
>
<View className="flex-row items-center justify-between">
<Text className="text-[15px] font-medium text-gray-500">Slot {index + 1}</Text>
{slots.length > 1 && (
<AppPressable onPress={() => handleRemoveSlot(index)} className="p-1">
<Ionicons name="trash-outline" size={20} color="#FF3B30" />
<AppPressable
onPress={() => handleRemoveSlot(index)}
className="rounded-full bg-red-50 p-2"
>
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
</AppPressable>
)}
</View>
<View className="mt-3 flex-row items-center">
<AppPressable
className="flex-1 rounded-lg bg-gray-100 px-3 py-2"
onPress={() => setShowTimePicker({ slotIndex: index, type: "start" })}
>
<Text className="text-[13px] text-gray-500">Start</Text>
<Text className="text-[17px] text-black">{formatTime12Hour(slot.startTime)}</Text>
</AppPressable>
{Platform.OS === "web" ? (
<View className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100">
<Text className="text-[13px] text-gray-500">Start</Text>
<input
type="time"
value={slot.startTime}
onChange={(e) => {
const newSlots = [...slots];
newSlots[index] = { ...newSlots[index], startTime: e.target.value };
setSlots(newSlots);
}}
style={{
fontSize: 17,
fontWeight: 500,
color: "#000",
backgroundColor: "transparent",
border: "none",
outline: "none",
fontFamily: "inherit",
width: "100%",
}}
/>
</View>
) : (
<AppPressable
className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100"
onPress={() => setShowTimePicker({ slotIndex: index, type: "start" })}
>
<Text className="text-[13px] text-gray-500">Start</Text>
<Text className="text-[17px] font-medium text-black">
{formatTime12Hour(slot.startTime)}
</Text>
</AppPressable>
)}
<Text className="mx-3 text-[17px] text-gray-400">–</Text>
<AppPressable
className="flex-1 rounded-lg bg-gray-100 px-3 py-2"
onPress={() => setShowTimePicker({ slotIndex: index, type: "end" })}
>
<Text className="text-[13px] text-gray-500">End</Text>
<Text className="text-[17px] text-black">{formatTime12Hour(slot.endTime)}</Text>
</AppPressable>
{Platform.OS === "web" ? (
<View className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100">
<Text className="text-[13px] text-gray-500">End</Text>
<input
type="time"
value={slot.endTime}
onChange={(e) => {
const newSlots = [...slots];
newSlots[index] = { ...newSlots[index], endTime: e.target.value };
setSlots(newSlots);
}}
style={{
fontSize: 17,
fontWeight: 500,
color: "#000",
backgroundColor: "transparent",
border: "none",
outline: "none",
fontFamily: "inherit",
width: "100%",
}}
/>
</View>
) : (
<AppPressable
className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100"
onPress={() => setShowTimePicker({ slotIndex: index, type: "end" })}
>
<Text className="text-[13px] text-gray-500">End</Text>
<Text className="text-[17px] font-medium text-black">
{formatTime12Hour(slot.endTime)}
</Text>
</AppPressable>
)}
</View>
</View>
))}
{/* Add Slot Button */}
<AppPressable
onPress={handleAddSlot}
className="flex-row items-center justify-center rounded-lg border border-dashed border-gray-300 py-3.5"
>
<Ionicons name="add-circle" size={20} color="#007AFF" />
<Text className="ml-2 text-[15px] font-medium text-[#007AFF]">Add Time Slot</Text>
<AppPressable onPress={handleAddSlot}>
<View className="flex-row items-center justify-center rounded-xl border-2 border-dashed border-gray-300 bg-gray-50 py-4">
<Ionicons name="add-circle" size={22} color="#007AFF" />
<Text className="ml-2 text-[17px] font-medium text-[#007AFF]">Add Time Slot</Text>
</View>
</AppPressable>
</>
)}
{/* Unavailable Message */}
{!isEnabled && (
<View className="items-center rounded-lg border border-gray-200 px-4 py-8">
<Ionicons name="moon-outline" size={40} color="#8E8E93" />
<Text className="mt-3 text-center text-[17px] text-gray-500">
You are unavailable on {dayName}
<View className="items-center rounded-xl border-2 border-dashed border-gray-200 px-4 py-12">
<View className="items-center justify-center rounded-full bg-gray-50 p-4">
<Ionicons name="moon-outline" size={32} color="#9CA3AF" />
</View>
<Text className="mt-4 text-center text-[17px] font-medium text-gray-900">
Unavailable
</Text>
<Text className="mt-1 text-center text-[15px] text-gray-400">
Toggle on to set availability
<Text className="mt-1 text-center text-[15px] text-gray-500">
You are currently unavailable on {dayName}.{"\n"}Toggle the switch above to add hours.
</Text>
</View>
)}
@@ -386,50 +496,27 @@ export const EditAvailabilityDayScreen = forwardRef<
{/* Time Picker Modal */}
<FullScreenModal
visible={!!showTimePicker}
animationType="slide"
animationType={Platform.OS === "web" ? "fade" : "slide"}
onRequestClose={() => setShowTimePicker(null)}
>
<View className="flex-1 bg-white">
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
<Text className="text-[17px] font-semibold">
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
</Text>
<AppPressable onPress={() => setShowTimePicker(null)}>
<Ionicons name="close" size={24} color="#8E8E93" />
</AppPressable>
</View>
<ScrollView>
{TIME_OPTIONS.map((time) => {
const slotIndex = showTimePicker?.slotIndex ?? 0;
const currentTime =
showTimePicker?.type === "start"
? slots[slotIndex]?.startTime
: slots[slotIndex]?.endTime;
const isSelected = currentTime === time;
return (
<AppPressable
key={time}
onPress={() => handleTimeSelect(time)}
className={`border-b border-gray-100 px-4 py-3.5 ${
isSelected ? "bg-blue-50" : ""
}`}
>
<View className="flex-row items-center justify-between">
<Text
className={`text-[17px] ${
isSelected ? "font-medium text-[#007AFF]" : "text-black"
}`}
>
{formatTime12Hour(time)}
</Text>
{isSelected && <Ionicons name="checkmark" size={20} color="#007AFF" />}
</View>
</AppPressable>
);
})}
</ScrollView>
</View>
{Platform.OS === "web" ? (
<TouchableOpacity
className="flex-1 items-center justify-center bg-black/50 p-4"
activeOpacity={1}
onPress={() => setShowTimePicker(null)}
>
<TouchableOpacity
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2"
activeOpacity={1}
onPress={(e) => e.stopPropagation()}
style={shadows.xl()}
>
{renderTimePickerContent()}
</TouchableOpacity>
</TouchableOpacity>
) : (
<View className="flex-1 bg-white p-2">{renderTimePickerContent()}</View>
)}
</FullScreenModal>
</ScrollView>
);
@@ -110,53 +110,53 @@ export const EditAvailabilityHoursScreen = forwardRef<unknown, EditAvailabilityH
Tap a day to edit its hours
</Text>
<View className="overflow-hidden rounded-lg border border-gray-200">
<View>
{DAYS.map((day, dayIndex) => {
const daySlots = availability[dayIndex] || [];
const isEnabled = daySlots.length > 0;
return (
<AppPressable
key={day}
className={`flex-row items-center px-4 py-3.5 ${
dayIndex > 0 ? "border-t border-gray-200" : ""
}`}
onPress={() => onDayPress(dayIndex)}
>
{/* Day status indicator */}
<AppPressable key={day} onPress={() => onDayPress(dayIndex)}>
<View
className={`mr-3 h-2.5 w-2.5 rounded-full ${
isEnabled ? "bg-green-500" : "bg-gray-300"
}`}
/>
{/* Day name */}
<Text
className={`w-24 text-[17px] font-medium ${
isEnabled ? "text-black" : "text-gray-400"
className={`mb-2.5 flex-row items-center rounded-xl border-2 px-4 py-4 ${
isEnabled ? "border-gray-200 bg-white" : "border-gray-100 bg-gray-50"
}`}
>
{day}
</Text>
{/* Day status indicator */}
<View
className={`mr-3 h-2.5 w-2.5 rounded-full ${
isEnabled ? "bg-green-500" : "bg-gray-300"
}`}
/>
{/* Time slots or unavailable */}
<View className="flex-1">
{isEnabled ? (
daySlots.map((slot, slotIndex) => (
<Text
key={`${slotIndex}-${slot.startTime}`}
className={`text-[15px] text-gray-500 ${slotIndex > 0 ? "mt-0.5" : ""}`}
>
{formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)}
</Text>
))
) : (
<Text className="text-[15px] text-gray-400">Unavailable</Text>
)}
{/* Day name */}
<Text
className={`w-28 text-[17px] font-medium ${
isEnabled ? "text-black" : "text-gray-400"
}`}
>
{day}
</Text>
{/* Time slots or unavailable */}
<View className="flex-1">
{isEnabled ? (
daySlots.map((slot, slotIndex) => (
<Text
key={`${slotIndex}-${slot.startTime}`}
className={`text-[15px] text-gray-600 ${slotIndex > 0 ? "mt-0.5" : ""}`}
>
{formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)}
</Text>
))
) : (
<Text className="text-[15px] text-gray-400">Unavailable</Text>
)}
</View>
{/* Chevron */}
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
</View>
{/* Chevron */}
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
</AppPressable>
);
})}
@@ -1,6 +1,15 @@
import { Ionicons } from "@expo/vector-icons";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import { Alert, KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native";
import {
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
import { FullScreenModal } from "@/components/FullScreenModal";
@@ -8,6 +17,7 @@ import { TIMEZONES as ALL_TIMEZONES } from "@/constants/timezones";
import type { Schedule } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import { showErrorAlert } from "@/utils/alerts";
import { shadows } from "@/utils/shadows";
// Format timezones for display
const TIMEZONES = ALL_TIMEZONES.map((tz) => ({
@@ -84,6 +94,57 @@ export const EditAvailabilityNameScreen = forwardRef<
const selectedTimezoneLabel = TIMEZONES.find((tz) => tz.id === timezone)?.label || timezone;
// Render timezone list content
const renderTimezoneContent = () => (
<>
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-4">
<Text className="text-[17px] font-semibold">Select Timezone</Text>
<AppPressable onPress={() => setShowTimezoneModal(false)}>
<Ionicons name="close" size={24} color="#8E8E93" />
</AppPressable>
</View>
<ScrollView className="px-4 py-3">
{TIMEZONES.map((tz) => (
<AppPressable
key={tz.id}
onPress={() => {
setTimezone(tz.id);
setShowTimezoneModal(false);
}}
>
<View
className={`mb-2.5 rounded-xl border-2 px-4 py-4 ${
tz.id === timezone
? "border-[#007AFF] bg-blue-50 shadow-md"
: "border-gray-200 bg-gray-50"
}`}
>
<View className="flex-row items-center justify-between">
<View className="flex-1">
<Text
className={`text-[17px] ${
tz.id === timezone
? "font-semibold text-[#007AFF]"
: "font-medium text-gray-900"
}`}
>
{tz.label}
</Text>
<Text className="mt-0.5 text-[13px] text-gray-500">{tz.id}</Text>
</View>
{tz.id === timezone && (
<View className="rounded-full bg-[#007AFF] p-1.5">
<Ionicons name="checkmark" size={14} color="#FFFFFF" />
</View>
)}
</View>
</View>
</AppPressable>
))}
</ScrollView>
</>
);
if (!schedule) {
return (
<View className="flex-1 items-center justify-center bg-white">
@@ -120,63 +181,49 @@ export const EditAvailabilityNameScreen = forwardRef<
<Text className="mb-2 text-[13px] font-medium uppercase tracking-wide text-gray-500">
Timezone
</Text>
<AppPressable
className="flex-row items-center justify-between rounded-lg border border-gray-300 bg-gray-50 px-4 py-3"
onPress={() => setShowTimezoneModal(true)}
>
<View className="flex-row items-center">
<Ionicons name="globe-outline" size={20} color="#007AFF" style={{ marginRight: 12 }} />
<View>
<Text className="text-[17px] text-black">{selectedTimezoneLabel}</Text>
<Text className="mt-0.5 text-[13px] text-gray-500">{timezone}</Text>
<AppPressable onPress={() => setShowTimezoneModal(true)}>
<View className="flex-row items-center justify-between rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-4">
<View className="flex-row items-center">
<Ionicons
name="globe-outline"
size={20}
color="#007AFF"
style={{ marginRight: 12 }}
/>
<View>
<Text className="text-[17px] text-black">{selectedTimezoneLabel}</Text>
<Text className="mt-0.5 text-[13px] text-gray-500">{timezone}</Text>
</View>
</View>
<Ionicons name="chevron-down" size={20} color="#C7C7CC" />
</View>
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
</AppPressable>
</ScrollView>
{/* Timezone Modal */}
<FullScreenModal
visible={showTimezoneModal}
animationType="slide"
animationType={Platform.OS === "web" ? "fade" : "slide"}
onRequestClose={() => setShowTimezoneModal(false)}
>
<View className="flex-1 bg-white">
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
<Text className="text-[17px] font-semibold">Select Timezone</Text>
<AppPressable onPress={() => setShowTimezoneModal(false)}>
<Ionicons name="close" size={24} color="#8E8E93" />
</AppPressable>
</View>
<ScrollView>
{TIMEZONES.map((tz) => (
<AppPressable
key={tz.id}
onPress={() => {
setTimezone(tz.id);
setShowTimezoneModal(false);
}}
className={`border-b border-gray-100 px-4 py-3.5 ${
tz.id === timezone ? "bg-blue-50" : ""
}`}
>
<View className="flex-row items-center justify-between">
<View>
<Text
className={`text-[17px] ${
tz.id === timezone ? "font-medium text-[#007AFF]" : "text-black"
}`}
>
{tz.label}
</Text>
<Text className="text-[13px] text-gray-500">{tz.id}</Text>
</View>
{tz.id === timezone && <Ionicons name="checkmark" size={20} color="#007AFF" />}
</View>
</AppPressable>
))}
</ScrollView>
</View>
{Platform.OS === "web" ? (
<TouchableOpacity
className="flex-1 items-center justify-center bg-black/50 p-4"
activeOpacity={1}
onPress={() => setShowTimezoneModal(false)}
>
<TouchableOpacity
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2"
activeOpacity={1}
onPress={(e) => e.stopPropagation()}
style={shadows.xl()}
>
{renderTimezoneContent()}
</TouchableOpacity>
</TouchableOpacity>
) : (
<View className="flex-1 bg-white p-2">{renderTimezoneContent()}</View>
)}
</FullScreenModal>
</KeyboardAvoidingView>
);
@@ -1,12 +1,22 @@
import { Ionicons } from "@expo/vector-icons";
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
import { Alert, ScrollView, Switch, Text, TextInput, View } from "react-native";
import {
Alert,
Platform,
ScrollView,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { AppPressable } from "@/components/AppPressable";
import { FullScreenModal } from "@/components/FullScreenModal";
import type { Schedule } from "@/services/calcom";
import { CalComAPIService } from "@/services/calcom";
import { showErrorAlert } from "@/utils/alerts";
import { shadows } from "@/utils/shadows";
// Generate time options (15-minute intervals)
const generateTimeOptions = () => {
@@ -78,6 +88,53 @@ export const EditAvailabilityOverrideScreen = forwardRef<
type: "start" | "end";
} | null>(null);
// Render modal content
const renderTimePickerContent = () => (
<>
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
<Text className="text-[17px] font-semibold">
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
</Text>
<AppPressable onPress={() => setShowTimePicker(null)}>
<Ionicons name="close" size={24} color="#8E8E93" />
</AppPressable>
</View>
<ScrollView className="px-4 py-3">
{TIME_OPTIONS.map((time) => {
const currentTime = showTimePicker?.type === "start" ? startTime : endTime;
const isSelected = currentTime === time;
return (
<AppPressable key={time} onPress={() => handleTimeSelect(time)}>
<View
className={`mb-2.5 rounded-xl border-2 px-4 py-4 ${
isSelected
? "border-[#007AFF] bg-blue-50 shadow-md"
: "border-gray-200 bg-gray-50"
}`}
>
<View className="flex-row items-center justify-between">
<Text
className={`text-[17px] ${
isSelected ? "font-semibold text-[#007AFF]" : "font-medium text-gray-900"
}`}
>
{formatTime12Hour(time)}
</Text>
{isSelected && (
<View className="rounded-full bg-[#007AFF] p-1.5">
<Ionicons name="checkmark" size={14} color="#FFFFFF" />
</View>
)}
</View>
</View>
</AppPressable>
);
})}
</ScrollView>
</>
);
// Initialize from existing override if editing
useEffect(() => {
if (schedule && isEditing && schedule.overrides && overrideIndex !== undefined) {
@@ -272,26 +329,43 @@ export const EditAvailabilityOverrideScreen = forwardRef<
>
{/* Date Input */}
<Text className="mb-2 text-[13px] font-medium text-gray-500">Date (YYYY-MM-DD)</Text>
<View className="mb-4 rounded-lg border border-gray-200 px-4 py-3">
<View className="mb-4 rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3">
<View className="flex-row items-center">
<Ionicons name="calendar" size={20} color="#007AFF" style={{ marginRight: 12 }} />
<TextInput
className="flex-1 text-[17px] text-black"
placeholder="2024-12-25"
placeholderTextColor="#9CA3AF"
value={selectedDate}
onChangeText={setSelectedDate}
/>
<Ionicons name="calendar-outline" size={20} color="#007AFF" style={{ marginRight: 12 }} />
{Platform.OS === "web" ? (
<input
type="date"
value={selectedDate}
onChange={(e) => setSelectedDate(e.target.value)}
style={{
flex: 1,
fontSize: 17,
color: "#000",
backgroundColor: "transparent",
border: "none",
outline: "none",
fontFamily: "inherit",
}}
/>
) : (
<TextInput
className="flex-1 text-[17px] text-black"
placeholder="2024-12-25"
placeholderTextColor="#9CA3AF"
value={selectedDate}
onChangeText={setSelectedDate}
/>
)}
</View>
{selectedDate && (
{selectedDate ? (
<Text className="mt-2 text-[15px] text-gray-500">
{formatDateForDisplay(selectedDate)}
</Text>
)}
) : null}
</View>
{/* Unavailable Toggle */}
<View className="mb-4 flex-row items-center justify-between rounded-lg border border-gray-200 px-4 py-3.5">
<View className="mb-4 flex-row items-center justify-between rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3.5">
<View className="flex-row items-center">
<Ionicons name="moon-outline" size={20} color="#FF3B30" style={{ marginRight: 12 }} />
<View>
@@ -311,25 +385,65 @@ export const EditAvailabilityOverrideScreen = forwardRef<
{!isUnavailable && (
<>
<Text className="mb-2 text-[13px] font-medium text-gray-500">Available Hours</Text>
<View className="mb-4 rounded-lg border border-gray-200 px-4 py-3.5">
<View className="mb-4 rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3.5">
<View className="flex-row items-center">
<AppPressable
className="flex-1 rounded-lg bg-gray-100 px-3 py-2"
onPress={() => setShowTimePicker({ type: "start" })}
>
<Text className="text-[13px] text-gray-500">Start Time</Text>
<Text className="text-[17px] text-black">{formatTime12Hour(startTime)}</Text>
</AppPressable>
{Platform.OS === "web" ? (
<View className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2">
<Text className="text-[13px] text-gray-500">Start Time</Text>
<input
type="time"
value={startTime}
onChange={(e) => setStartTime(e.target.value)}
style={{
fontSize: 17,
color: "#000",
backgroundColor: "transparent",
border: "none",
outline: "none",
fontFamily: "inherit",
width: "100%",
}}
/>
</View>
) : (
<AppPressable
className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2"
onPress={() => setShowTimePicker({ type: "start" })}
>
<Text className="text-[13px] text-gray-500">Start Time</Text>
<Text className="text-[17px] text-black">{formatTime12Hour(startTime)}</Text>
</AppPressable>
)}
<Text className="mx-4 text-[17px] text-gray-400">–</Text>
<AppPressable
className="flex-1 rounded-lg bg-gray-100 px-3 py-2"
onPress={() => setShowTimePicker({ type: "end" })}
>
<Text className="text-[13px] text-gray-500">End Time</Text>
<Text className="text-[17px] text-black">{formatTime12Hour(endTime)}</Text>
</AppPressable>
{Platform.OS === "web" ? (
<View className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2">
<Text className="text-[13px] text-gray-500">End Time</Text>
<input
type="time"
value={endTime}
onChange={(e) => setEndTime(e.target.value)}
style={{
fontSize: 17,
color: "#000",
backgroundColor: "transparent",
border: "none",
outline: "none",
fontFamily: "inherit",
width: "100%",
}}
/>
</View>
) : (
<AppPressable
className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2"
onPress={() => setShowTimePicker({ type: "end" })}
>
<Text className="text-[13px] text-gray-500">End Time</Text>
<Text className="text-[17px] text-black">{formatTime12Hour(endTime)}</Text>
</AppPressable>
)}
</View>
</View>
</>
@@ -341,41 +455,41 @@ export const EditAvailabilityOverrideScreen = forwardRef<
<Text className="mb-2 mt-4 text-[13px] font-medium text-gray-500">
Existing Overrides
</Text>
<View className="overflow-hidden rounded-lg border border-gray-200">
<View>
{schedule.overrides.map((override, index) => (
<AppPressable
key={override.date}
className={`flex-row items-center justify-between px-4 py-3 ${
index > 0 ? "border-t border-gray-200" : ""
}`}
className="mb-2.5"
onPress={() => onEditOverride?.(index)}
>
<View className="flex-1">
<Text className="text-[15px] text-black">
{formatDateForDisplay(override.date ?? "")}
</Text>
{override.startTime === "00:00" && override.endTime === "00:00" ? (
<Text className="text-[13px] text-red-500">Unavailable</Text>
) : (
<Text className="text-[13px] text-gray-500">
{formatTime12Hour(override.startTime ?? "00:00")} –{" "}
{formatTime12Hour(override.endTime ?? "00:00")}
<View className="flex-row items-center justify-between rounded-xl border-2 border-gray-200 bg-white px-4 py-3.5">
<View className="flex-1">
<Text className="text-[15px] font-medium text-black">
{formatDateForDisplay(override.date ?? "")}
</Text>
)}
</View>
<View className="flex-row items-center">
<AppPressable
className="rounded-full bg-red-100 p-2"
onPress={() => handleDeleteOverride(index)}
>
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
</AppPressable>
<AppPressable
className="ml-3 rounded-full bg-gray-100 p-2"
onPress={() => onEditOverride?.(index)}
>
<Ionicons name="chevron-forward" size={18} color="#8E8E93" />
</AppPressable>
{override.startTime === "00:00" && override.endTime === "00:00" ? (
<Text className="mt-0.5 text-[13px] text-red-500">Unavailable</Text>
) : (
<Text className="mt-0.5 text-[13px] text-gray-500">
{formatTime12Hour(override.startTime ?? "00:00")} –{" "}
{formatTime12Hour(override.endTime ?? "00:00")}
</Text>
)}
</View>
<View className="flex-row items-center">
<AppPressable
className="rounded-full bg-red-50 p-2"
onPress={() => handleDeleteOverride(index)}
>
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
</AppPressable>
<Ionicons
style={{ marginLeft: 12 }}
name="chevron-forward"
size={18}
color="#C7C7CC"
/>
</View>
</View>
</AppPressable>
))}
@@ -386,46 +500,27 @@ export const EditAvailabilityOverrideScreen = forwardRef<
{/* Time Picker Modal */}
<FullScreenModal
visible={!!showTimePicker}
animationType="slide"
animationType={Platform.OS === "web" ? "fade" : "slide"}
onRequestClose={() => setShowTimePicker(null)}
>
<View className="flex-1 bg-white">
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
<Text className="text-[17px] font-semibold">
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
</Text>
<AppPressable onPress={() => setShowTimePicker(null)}>
<Ionicons name="close" size={24} color="#8E8E93" />
</AppPressable>
</View>
<ScrollView>
{TIME_OPTIONS.map((time) => {
const currentTime = showTimePicker?.type === "start" ? startTime : endTime;
const isSelected = currentTime === time;
return (
<AppPressable
key={time}
onPress={() => handleTimeSelect(time)}
className={`border-b border-gray-100 px-4 py-3.5 ${
isSelected ? "bg-blue-50" : ""
}`}
>
<View className="flex-row items-center justify-between">
<Text
className={`text-[17px] ${
isSelected ? "font-medium text-[#007AFF]" : "text-black"
}`}
>
{formatTime12Hour(time)}
</Text>
{isSelected && <Ionicons name="checkmark" size={20} color="#007AFF" />}
</View>
</AppPressable>
);
})}
</ScrollView>
</View>
{Platform.OS === "web" ? (
<TouchableOpacity
className="flex-1 items-center justify-center bg-black/50 p-4"
activeOpacity={1}
onPress={() => setShowTimePicker(null)}
>
<TouchableOpacity
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2"
activeOpacity={1}
onPress={(e) => e.stopPropagation()}
style={shadows.xl()}
>
{renderTimePickerContent()}
</TouchableOpacity>
</TouchableOpacity>
) : (
<View className="flex-1 bg-white p-2">{renderTimePickerContent()}</View>
)}
</FullScreenModal>
</ScrollView>
);