feat(companion): dark mode (#27305)
* feat(companion): add dark mode support using NativeWind system preference - Set userInterfaceStyle to 'automatic' in app.json to follow system preference - Add useColorScheme hook from NativeWind to detect color scheme - Update root layout with dynamic colors for StatusBar, containers, and modals - Update tabs layout with dynamic colors for tab bar and icons - Update all Stack.Screen options to use dynamic background colors and blur effects - Support both iOS and Android with appropriate dark mode colors Co-Authored-By: peer@cal.com <peer@cal.com> * fix(companion): use NativeWind dark: variant classes for automatic dark mode Use dark: Tailwind variant classes instead of conditional logic based on useColorScheme for the container background. This ensures NativeWind automatically applies dark mode styles based on system preference. Co-Authored-By: peer@cal.com <peer@cal.com> * fix(companion): use darkMode 'media' for automatic dark mode detection - Changed darkMode from 'class' to 'media' in tailwind.config.js - Updated global.css to use @media (prefers-color-scheme: dark) instead of .dark selector - This ensures NativeWind properly detects system color scheme on iOS/Android Co-Authored-By: peer@cal.com <peer@cal.com> * dark mode starting * feat(companion): add dark mode support to core infrastructure (Phase 1) * feat(companion): add dark mode support to tab pages (Phase 2) * fix(companion): add dark mode support to booking list components for font visibility * fix(companion): add dark mode support to EventTypeListItem components * event types and bookings page * Availability list page * more page, and some ui fixes * feat(companion): add dark mode support to detail screens and modals - BookingDetailScreen (iOS & Android): dynamic colors for all UI elements - AvailabilityDetailScreen (iOS & Android): dark mode backgrounds and text - profile-sheet (iOS & Android): dark mode support for profile modal - RescheduleScreen (all platforms): dark mode for date/time pickers - EditLocationScreen (iOS & Android): dark mode for location editor - AddGuestsScreen: partial dark mode implementation (in progress) Uses iOS system colors: #000000 (black), #1C1C1E (secondary), #FFFFFF (white), #38383A (border), #8E8E93 (text secondary) * fix typecheck and lint * feat(companion): add dark mode support to remaining screens - ScreenWrapper: Fix hardcoded #800020 color with dynamic destructive color - EditAvailabilityDayScreen: Add dark mode for Android/Web and iOS - EditAvailabilityHoursScreen: Add dark mode for Android/Web and iOS - EditAvailabilityNameScreen: Add dark mode for Android/Web and iOS - EditAvailabilityOverrideScreen: Add dark mode for Web, iOS, and Android - MarkNoShowScreen: Add dark mode with dynamic colors for no-show states - MeetingSessionDetailsScreen: Add dark mode for session details - ViewRecordingsScreen: Add dark mode for recordings list Uses iOS system colors (#000000, #1C1C1E, #38383A, #8E8E93) and useColorScheme hook for automatic theme detection. * fix(companion): fix iOS modal dark mode backgrounds for transparent glass UI * fix(companion): fix dark mode for booking action screens in transparent background mode * fix(companion): fix Android dark mode for transparent background in EditAvailabilityOverrideScreen * refactor(companion): centralize dark mode colors and align with main website - Update tailwind.config.js with centralized color definitions - Create constants/colors.ts for inline style color references - Replace iOS system colors with main website neutral grays: - #1C1C1E -> #171717 (secondary dark background) - #38383A -> #4D4D4D (dark border) - #8E8E93 -> #A3A3A3 (secondary text dark) - Keep pure black (#000000) for main backgrounds (OLED friendly) - Add semantic color mappings for consistent UI styling * feat(companion): complete dark mode implementation across all priority files - Priority 1: Updated SettingsUI.tsx, BasicsTab.tsx, LimitsTab.tsx, AdvancedTab.tsx, RecurringTab.tsx, AvailabilityTab.tsx, event-type-detail.tsx - Priority 2: Updated edit-availability-hours/name/override/day.tsx, oauth/callback.tsx - Priority 3: Updated AvailabilityListScreen.tsx with dark mode for modals and list items - Priority 4: Updated BookingActionsModal.tsx, BookingActionsModal.ios.tsx, GlassModalHeader.tsx, LoginScreen.tsx, LocationsList.tsx, SearchHeader.tsx, toast.tsx Dark mode colors used: - Main bg: #000000 (cal.bg.dark) - Secondary bg: #171717 (cal.bg.secondaryDark) - Muted bg: #262626 (cal.bg.mutedDark) - Borders: #4D4D4D (cal.border.dark) - Secondary text: #A3A3A3 (cal.text.secondaryDark) * fix(companion): add dark mode support to iOS header, GlassView modals, and main content background * fix(companion): add dark mode support to toggle/switch components * fix(companion): use iOS green (#34C759) for toggle ON state in dark mode * fix(companion): use white track + black thumb for dark mode toggles * fix(companion): add dark mode support to login page logo and button * dark mode to many pages, centralize the colors * dark mode skelaton * Availability pages android * toggles * headers * red color for dark mode * copy button * white flash on bottom ui native ios sheet * bottom sheet buttons * bottom sheet in gray color * bottom sheet again.. yeahh * toggle green * toggle green * fix lint and typechecks * fix exntension error * Update companion/components/event-type-detail/tabs/LimitsTabDatePicker.android.tsx Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * address cubic comments --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: peer@cal.com <peer@cal.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
cubic-dev-ai[bot]
Devin AI
peer@cal.com
parent
39f97e7326
commit
0e6bf54fa2
+5
-3
@@ -3,10 +3,10 @@
|
||||
"name": "Calcom",
|
||||
"slug": "calcom-companion",
|
||||
"scheme": "expo-wxt-app",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"splash": {
|
||||
"image": "./assets/splash-icon.png",
|
||||
@@ -17,6 +17,7 @@
|
||||
"supportsTablet": false,
|
||||
"deploymentTarget": "18.0",
|
||||
"bundleIdentifier": "com.cal.companion",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"infoPlist": {
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
},
|
||||
@@ -33,7 +34,8 @@
|
||||
"edgeToEdgeEnabled": true,
|
||||
"package": "com.calcom.companion",
|
||||
"softwareKeyboardLayoutMode": "pan",
|
||||
"usesCleartextTraffic": false
|
||||
"usesCleartextTraffic": false,
|
||||
"userInterfaceStyle": "automatic"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useColorScheme } from "react-native";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useRef } from "react";
|
||||
import {
|
||||
AvailabilityDetailScreen,
|
||||
type AvailabilityDetailScreenHandle,
|
||||
} from "@/components/screens/AvailabilityDetailScreen";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
// Type for action handlers exposed by AvailabilityDetailScreen.ios.tsx
|
||||
type ActionHandlers = {
|
||||
@@ -14,6 +16,9 @@ type ActionHandlers = {
|
||||
export default function AvailabilityDetailIOS() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
// Ref to store action handlers from AvailabilityDetailScreen
|
||||
const actionHandlersRef = useRef<ActionHandlers | null>(null);
|
||||
@@ -67,13 +72,18 @@ export default function AvailabilityDetailIOS() {
|
||||
headerBackButtonDisplayMode: "default",
|
||||
headerTitle: "",
|
||||
headerStyle: {
|
||||
backgroundColor: "#f2f2f7",
|
||||
backgroundColor: isDark ? theme.background : theme.backgroundMuted,
|
||||
},
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header style={{ shadowColor: "transparent", backgroundColor: "#f2f2f7" }}>
|
||||
<Stack.Header
|
||||
style={{
|
||||
shadowColor: "transparent",
|
||||
backgroundColor: isDark ? theme.background : theme.backgroundMuted,
|
||||
}}
|
||||
>
|
||||
<Stack.Header.Right>
|
||||
{/* Edit Menu */}
|
||||
<Stack.Header.Menu>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { Text, TouchableOpacity } from "react-native";
|
||||
import { Text, useColorScheme } from "react-native";
|
||||
import {
|
||||
AvailabilityDetailScreen,
|
||||
type AvailabilityDetailScreenHandle,
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
|
||||
// Type for action handlers exposed by AvailabilityDetailScreen
|
||||
type ActionHandlers = {
|
||||
@@ -24,6 +26,9 @@ type ActionHandlers = {
|
||||
export default function AvailabilityDetail() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
// Ref to store action handlers from AvailabilityDetailScreen
|
||||
const actionHandlersRef = useRef<ActionHandlers | null>(null);
|
||||
@@ -72,47 +77,82 @@ export default function AvailabilityDetail() {
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Availability",
|
||||
headerBackTitle: "Availability",
|
||||
title: "",
|
||||
headerBackTitle: "", // Hide default back title if it appears
|
||||
headerStyle: {
|
||||
backgroundColor: "#f2f2f7",
|
||||
backgroundColor: isDark ? "black" : "white",
|
||||
},
|
||||
headerShadowVisible: false,
|
||||
headerLeft: () => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
className="mr-2 h-10 flex-row items-center justify-center rounded-full border border-[#E5E5E5] bg-white px-3 dark:border-[#262626] dark:bg-[#171717]"
|
||||
>
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={20}
|
||||
color={isDark ? "#FFFFFF" : "#000000"}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
<Text className={`text-[15px] font-medium ${isDark ? "text-white" : "text-black"}`}>
|
||||
Availability
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
padding: 8,
|
||||
marginRight: -8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal-circle" size={24} color="#000000" />
|
||||
</TouchableOpacity>
|
||||
<AppPressable className="h-10 flex-row items-center justify-center rounded-full border border-[#E5E5E5] bg-white px-4 dark:border-[#262626] dark:bg-[#171717]">
|
||||
<Text
|
||||
className={`text-[15px] font-medium ${isDark ? "text-white" : "text-black"}`}
|
||||
>
|
||||
Edit
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</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>
|
||||
<Ionicons
|
||||
name="pencil-outline"
|
||||
size={18}
|
||||
color={isDark ? theme.text : "#000"}
|
||||
/>
|
||||
<Text className="ml-2 text-[15px] text-black dark:text-white">
|
||||
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>
|
||||
<Ionicons name="time-outline" size={18} color={isDark ? theme.text : "#000"} />
|
||||
<Text className="ml-2 text-[15px] text-black dark:text-white">
|
||||
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>
|
||||
<Ionicons
|
||||
name="calendar-outline"
|
||||
size={18}
|
||||
color={isDark ? theme.text : "#000"}
|
||||
/>
|
||||
<Text className="ml-2 text-[15px] text-black dark:text-white">
|
||||
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>
|
||||
<Ionicons name="star-outline" size={18} color={isDark ? theme.text : "#000"} />
|
||||
<Text className="ml-2 text-[15px] text-black dark:text-white">
|
||||
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>
|
||||
<Ionicons name="trash-outline" size={18} color={theme.error} />
|
||||
<Text className="ml-2 text-[15px]" style={{ color: theme.error }}>
|
||||
Delete Schedule
|
||||
</Text>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Image } from "expo-image";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert, Platform, Pressable } from "react-native";
|
||||
import { Alert, Platform, Pressable, useColorScheme } from "react-native";
|
||||
import { AvailabilityListScreen } from "@/components/screens/AvailabilityListScreen";
|
||||
import { useCreateSchedule, useUserProfile } from "@/hooks";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
@@ -15,6 +15,8 @@ export default function Availability() {
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const { mutate: createScheduleMutation } = useCreateSchedule();
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const handleCreateNew = () => {
|
||||
// Use native iOS Alert.prompt for a native look
|
||||
@@ -93,7 +95,7 @@ export default function Availability() {
|
||||
<>
|
||||
<Stack.Header
|
||||
style={{ backgroundColor: "transparent", shadowColor: "transparent" }}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : "light"}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : isDark ? "dark" : "light"}
|
||||
hidden={Platform.OS === "android" || Platform.OS === "web"}
|
||||
>
|
||||
<Stack.Header.Title large>Availability</Stack.Header.Title>
|
||||
@@ -127,7 +129,7 @@ export default function Availability() {
|
||||
placeholder="Search schedules"
|
||||
onChangeText={(e) => setSearchQuery(e.nativeEvent.text)}
|
||||
obscureBackground={false}
|
||||
barTintColor="#fff"
|
||||
barTintColor={isDark ? "#000" : "#fff"}
|
||||
/>
|
||||
</Stack.Header>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useBookingByUid } from "@/hooks/useBookings";
|
||||
@@ -63,6 +64,7 @@ const getMeetingUrl = (booking: Booking | null): string | null => {
|
||||
export default function BookingDetailIOS() {
|
||||
const { uid } = useLocalSearchParams<{ uid: string }>();
|
||||
const { userInfo } = useAuth();
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
// Use React Query hook for booking data - single source of truth
|
||||
const { data: booking, isLoading, error, refetch, isRefetching } = useBookingByUid(uid);
|
||||
@@ -295,7 +297,11 @@ export default function BookingDetailIOS() {
|
||||
</Stack.Header.Menu>
|
||||
|
||||
{meetingUrl && (
|
||||
<Stack.Header.Button onPress={handleJoinMeeting} variant="prominent" tintColor="#000">
|
||||
<Stack.Header.Button
|
||||
onPress={handleJoinMeeting}
|
||||
variant="prominent"
|
||||
tintColor={colorScheme === "dark" ? "#FFF" : "#000"}
|
||||
>
|
||||
Join
|
||||
</Stack.Header.Button>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useNavigation, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { Platform, Text, View, useColorScheme } from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useBookingByUid } from "@/hooks/useBookings";
|
||||
import { showErrorAlert, showInfoAlert } from "@/utils/alerts";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts";
|
||||
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
|
||||
// Empty actions result for when no booking is loaded
|
||||
const EMPTY_ACTIONS: BookingActionsResult = {
|
||||
@@ -19,6 +33,22 @@ const EMPTY_ACTIONS: BookingActionsResult = {
|
||||
markNoShow: { visible: false, enabled: false },
|
||||
};
|
||||
|
||||
const getMeetingUrl = (booking: Booking | null): string | null => {
|
||||
if (!booking) return null;
|
||||
|
||||
const videoCallUrl = booking.responses?.videoCallUrl;
|
||||
if (typeof videoCallUrl === "string" && videoCallUrl.startsWith("http")) {
|
||||
return videoCallUrl;
|
||||
}
|
||||
|
||||
const location = booking.location;
|
||||
if (typeof location === "string" && location.startsWith("http")) {
|
||||
return location;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Type for action handlers exposed by BookingDetailScreen
|
||||
type ActionHandlers = {
|
||||
openRescheduleModal: () => void;
|
||||
@@ -30,6 +60,29 @@ type ActionHandlers = {
|
||||
handleCancelBooking: () => void;
|
||||
};
|
||||
|
||||
const getIconName = (sfSymbol: string): keyof typeof Ionicons.glyphMap => {
|
||||
switch (sfSymbol) {
|
||||
case "calendar":
|
||||
return "calendar";
|
||||
case "location":
|
||||
return "location";
|
||||
case "person.badge.plus":
|
||||
return "person-add";
|
||||
case "video":
|
||||
return "videocam";
|
||||
case "info.circle":
|
||||
return "information-circle";
|
||||
case "eye.slash":
|
||||
return "eye-off";
|
||||
case "flag":
|
||||
return "flag";
|
||||
case "xmark.circle":
|
||||
return "close-circle";
|
||||
default:
|
||||
return "ellipsis-horizontal";
|
||||
}
|
||||
};
|
||||
|
||||
export default function BookingDetail() {
|
||||
"use no memo";
|
||||
const { uid } = useLocalSearchParams<{ uid: string }>();
|
||||
@@ -218,11 +271,209 @@ export default function BookingDetail() {
|
||||
handleCancel,
|
||||
]);
|
||||
|
||||
const router = useRouter();
|
||||
const navigation = useNavigation();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDarkMode = colorScheme === "dark";
|
||||
|
||||
const meetingUrl = useMemo(() => getMeetingUrl(booking ?? null), [booking]);
|
||||
|
||||
const handleJoinMeeting = useCallback(() => {
|
||||
if (meetingUrl) {
|
||||
openInAppBrowser(meetingUrl, "meeting link");
|
||||
}
|
||||
}, [meetingUrl]);
|
||||
|
||||
const handleCopyMeetingLink = useCallback(async () => {
|
||||
if (meetingUrl) {
|
||||
await Clipboard.setStringAsync(meetingUrl);
|
||||
showSuccessAlert("Copied", "Meeting link copied to clipboard");
|
||||
}
|
||||
}, [meetingUrl]);
|
||||
|
||||
const renderHeaderLeft = useCallback(() => {
|
||||
let backButtonLabel = "Back";
|
||||
|
||||
// Check both start and startTime properties
|
||||
const startTime = booking?.start || booking?.startTime;
|
||||
|
||||
if (startTime) {
|
||||
const date = new Date(startTime);
|
||||
if (!Number.isNaN(date.getTime())) {
|
||||
backButtonLabel = date.toLocaleDateString("en-US", { month: "long" });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
className="mr-2 h-10 flex-row items-center justify-center rounded-full border border-[#E5E5E5] bg-white px-3 dark:border-[#262626] dark:bg-[#171717]"
|
||||
>
|
||||
<Ionicons
|
||||
name="chevron-back"
|
||||
size={20}
|
||||
color={isDarkMode ? "#FFFFFF" : "#000000"}
|
||||
style={{ marginRight: 4 }}
|
||||
/>
|
||||
<Text className={`text-[15px] font-medium ${isDarkMode ? "text-white" : "text-black"}`}>
|
||||
{backButtonLabel}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
);
|
||||
}, [booking?.start, booking?.startTime, router, isDarkMode]);
|
||||
|
||||
const renderHeaderRight = useCallback(
|
||||
() => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<View className="flex-row items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<AppPressable className="h-10 w-10 items-center justify-center rounded-full border border-[#E5E5E5] bg-white dark:border-[#262626] dark:bg-[#171717]">
|
||||
<Ionicons
|
||||
name="ellipsis-horizontal"
|
||||
size={20}
|
||||
color={isDarkMode ? "#FFFFFF" : "#000000"}
|
||||
/>
|
||||
</AppPressable>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
insets={{ top: 60, bottom: 20, left: 12, right: 12 }}
|
||||
sideOffset={8}
|
||||
className="w-56"
|
||||
align="end"
|
||||
>
|
||||
{meetingUrl && (
|
||||
<>
|
||||
<DropdownMenuItem onPress={handleCopyMeetingLink}>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Ionicons
|
||||
name="copy-outline"
|
||||
size={18}
|
||||
color={isDarkMode ? "#FFFFFF" : "#000000"}
|
||||
/>
|
||||
<Text className="text-base text-[#000000] dark:text-white">
|
||||
Copy meeting link
|
||||
</Text>
|
||||
</View>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{bookingActionsSections.editEvent.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuLabel>Edit Event</DropdownMenuLabel>
|
||||
{bookingActionsSections.editEvent.map((action) => (
|
||||
<DropdownMenuItem key={action.id} onPress={action.onPress}>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Ionicons
|
||||
name={getIconName(action.icon)}
|
||||
size={18}
|
||||
color={isDarkMode ? "#FFFFFF" : "#000000"}
|
||||
/>
|
||||
<Text className="text-base text-[#000000] dark:text-white">
|
||||
{action.label}
|
||||
</Text>
|
||||
</View>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{bookingActionsSections.afterEvent.length > 0 && (
|
||||
<>
|
||||
<DropdownMenuLabel>After Event</DropdownMenuLabel>
|
||||
{bookingActionsSections.afterEvent.map((action) => (
|
||||
<DropdownMenuItem key={action.id} onPress={action.onPress}>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Ionicons
|
||||
name={getIconName(action.icon)}
|
||||
size={18}
|
||||
color={isDarkMode ? "#FFFFFF" : "#000000"}
|
||||
/>
|
||||
<Text className="text-base text-[#000000] dark:text-white">
|
||||
{action.label}
|
||||
</Text>
|
||||
</View>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<DropdownMenuLabel>Danger Zone</DropdownMenuLabel>
|
||||
{bookingActionsSections.standalone.map((action) => (
|
||||
<DropdownMenuItem
|
||||
key={action.id}
|
||||
onPress={action.onPress}
|
||||
variant={action.destructive ? "destructive" : "default"}
|
||||
>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Ionicons
|
||||
name={getIconName(action.icon)}
|
||||
size={18}
|
||||
color={action.destructive ? "#EF4444" : isDarkMode ? "#FFFFFF" : "#000000"}
|
||||
/>
|
||||
<Text
|
||||
className={`text-base ${
|
||||
action.destructive ? "text-red-500" : "text-[#000000] dark:text-white"
|
||||
}`}
|
||||
>
|
||||
{action.label}
|
||||
</Text>
|
||||
</View>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
{meetingUrl && (
|
||||
<AppPressable
|
||||
onPress={handleJoinMeeting}
|
||||
className={`h-9 flex-row items-center justify-center rounded-full border px-5 ${
|
||||
isDarkMode ? "border-white bg-white" : "border-black bg-black"
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
className={`text-[15px] font-medium ${isDarkMode ? "text-black" : "text-white"}`}
|
||||
>
|
||||
Join
|
||||
</Text>
|
||||
</AppPressable>
|
||||
)}
|
||||
</View>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[isDarkMode, meetingUrl, handleCopyMeetingLink, bookingActionsSections, handleJoinMeeting]
|
||||
);
|
||||
|
||||
// Force header update on Android/Web
|
||||
useEffect(() => {
|
||||
if (Platform.OS === "android" || Platform.OS === "web") {
|
||||
navigation.setOptions({
|
||||
headerRight: renderHeaderRight,
|
||||
headerLeft: renderHeaderLeft,
|
||||
});
|
||||
}
|
||||
}, [navigation, renderHeaderRight, renderHeaderLeft]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerBackButtonDisplayMode: "minimal",
|
||||
title: "",
|
||||
headerShown: Platform.OS !== "ios",
|
||||
headerStyle: {
|
||||
backgroundColor: isDarkMode ? "black" : "white",
|
||||
},
|
||||
headerShadowVisible: false,
|
||||
headerLeft:
|
||||
Platform.OS === "android" || Platform.OS === "web" ? renderHeaderLeft : undefined,
|
||||
headerRight:
|
||||
Platform.OS === "android" || Platform.OS === "web" ? renderHeaderRight : undefined,
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NativeStackHeaderItemMenuAction } from "@react-navigation/native-s
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { useColorScheme } from "react-native";
|
||||
|
||||
import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen";
|
||||
import { useEventTypes } from "@/hooks";
|
||||
@@ -22,6 +23,10 @@ export default function Bookings() {
|
||||
}
|
||||
);
|
||||
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const textColor = isDark ? "#FFFFFF" : "#000000";
|
||||
|
||||
const handleSearch = (query: string) => {
|
||||
setSearchQuery(query);
|
||||
};
|
||||
@@ -62,7 +67,7 @@ export default function Bookings() {
|
||||
label: currentFilterOption?.label || "Filter",
|
||||
labelStyle: {
|
||||
fontWeight: "600",
|
||||
color: "#000000",
|
||||
color: textColor,
|
||||
},
|
||||
menu: {
|
||||
title: "Filter by Status",
|
||||
@@ -123,7 +128,7 @@ export default function Bookings() {
|
||||
},
|
||||
labelStyle: {
|
||||
fontWeight: "600",
|
||||
color: "#000000",
|
||||
color: textColor,
|
||||
},
|
||||
menu: {
|
||||
title: menuItems.length > 0 ? "Filter by Event Type" : "No Event Types",
|
||||
@@ -149,7 +154,7 @@ export default function Bookings() {
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Bookings",
|
||||
headerBlurEffect: isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect: isLiquidGlassAvailable() ? undefined : isDark ? "dark" : "light",
|
||||
headerStyle: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
@@ -158,7 +163,7 @@ export default function Bookings() {
|
||||
placeholder: "Search bookings",
|
||||
onChangeText: (e) => handleSearch(e.nativeEvent.text),
|
||||
obscureBackground: false,
|
||||
barTintColor: "#fff",
|
||||
barTintColor: isDark ? "#171717" : "#fff",
|
||||
},
|
||||
unstable_headerRightItems: () => {
|
||||
const eventTypeFilterMenu = buildEventTypeFilterMenu();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useState } from "react";
|
||||
import { Text, TextInput, View } from "react-native";
|
||||
import { Text, TextInput, useColorScheme, View } from "react-native";
|
||||
import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen";
|
||||
import { Header } from "@/components/Header";
|
||||
import {
|
||||
@@ -12,11 +12,15 @@ import {
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
|
||||
import { useEventTypes } from "@/hooks";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
export default function Bookings() {
|
||||
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();
|
||||
@@ -55,7 +59,13 @@ export default function Bookings() {
|
||||
selectedEventTypeId !== null ? selectedEventTypeLabel || "Event Type" : "Filter";
|
||||
|
||||
return (
|
||||
<View className="border-b border-gray-300 bg-gray-100 px-2 py-2 md:px-4">
|
||||
<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>
|
||||
@@ -63,21 +73,31 @@ export default function Bookings() {
|
||||
<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="#333" />
|
||||
<Ionicons name="options-outline" size={14} color={theme.textSecondary} />
|
||||
<Text
|
||||
className={`text-sm ${selectedEventTypeId !== null ? "text-[#000000] font-semibold" : "text-[#333]"}`}
|
||||
style={{ marginLeft: 4 }}
|
||||
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="#333" style={{ marginLeft: 2 }} />
|
||||
<Ionicons
|
||||
name="chevron-down"
|
||||
size={12}
|
||||
color={theme.textSecondary}
|
||||
style={{ marginLeft: 2 }}
|
||||
/>
|
||||
</AppPressable>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -92,7 +112,9 @@ export default function Bookings() {
|
||||
checked={selectedEventTypeId === null}
|
||||
onCheckedChange={() => handleEventTypeSelect(null)}
|
||||
>
|
||||
<Text className="text-base">All Event Types</Text>
|
||||
<Text className="text-base" style={{ color: theme.text }}>
|
||||
All Event Types
|
||||
</Text>
|
||||
</DropdownMenuCheckboxItem>
|
||||
|
||||
{/* Event type options */}
|
||||
@@ -102,7 +124,7 @@ export default function Bookings() {
|
||||
checked={selectedEventTypeId === eventType.id}
|
||||
onCheckedChange={() => handleEventTypeSelect(eventType.id, eventType.title)}
|
||||
>
|
||||
<Text className="text-base" numberOfLines={1}>
|
||||
<Text className="text-base" style={{ color: theme.text }} numberOfLines={1}>
|
||||
{eventType.title}
|
||||
</Text>
|
||||
</DropdownMenuCheckboxItem>
|
||||
@@ -111,7 +133,7 @@ export default function Bookings() {
|
||||
{/* Loading state */}
|
||||
{eventTypesLoading && eventTypes.length === 0 && (
|
||||
<DropdownMenuCheckboxItem checked={false} onCheckedChange={() => {}}>
|
||||
<Text className="text-base text-gray-500">Loading...</Text>
|
||||
<Text className="text-base text-gray-500 dark:text-gray-400">Loading...</Text>
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
@@ -120,8 +142,13 @@ export default function Bookings() {
|
||||
<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="#8E8E93"
|
||||
placeholderTextColor={theme.textSecondary}
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
autoCapitalize="none"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { Stack, useLocalSearchParams, useNavigation, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
Animated,
|
||||
Modal,
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
Switch,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
@@ -138,6 +140,8 @@ const tabs: Tab[] = [
|
||||
|
||||
export default function EventTypeDetail() {
|
||||
const router = useRouter();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDarkMode = colorScheme === "dark";
|
||||
const { id, title, description, duration, slug } = useLocalSearchParams<{
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -1179,7 +1183,7 @@ export default function EventTypeDetail() {
|
||||
return Object.keys(payload).length > 0;
|
||||
}, [currentFormState, eventTypeData, id]);
|
||||
|
||||
const handleSave = async () => {
|
||||
const handleSave = useCallback(async () => {
|
||||
if (!id) {
|
||||
showErrorAlert("Error", "Event type ID is missing");
|
||||
return;
|
||||
@@ -1268,108 +1272,159 @@ export default function EventTypeDetail() {
|
||||
showSuccessAlert("Success", "Event type updated successfully");
|
||||
// No need to manually refresh - cache is updated by the mutation hook
|
||||
}
|
||||
};
|
||||
}, [
|
||||
id,
|
||||
eventTitle,
|
||||
eventSlug,
|
||||
eventDuration,
|
||||
locations,
|
||||
selectedSchedule?.id,
|
||||
eventDescription,
|
||||
isHidden,
|
||||
createEventType,
|
||||
router,
|
||||
currentFormState,
|
||||
eventTypeData,
|
||||
updateEventType,
|
||||
]);
|
||||
|
||||
const headerTitle = id === "new" ? "Create Event Type" : truncateTitle(title);
|
||||
const saveButtonText = id === "new" ? "Create" : "Save";
|
||||
|
||||
const renderHeaderLeft = () => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable onPress={() => router.back()} className="px-2 py-2">
|
||||
<Ionicons name="close" size={24} color="#000000" />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
const renderHeaderLeft = useCallback(
|
||||
() => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
className="mr-2 h-10 w-10 items-center justify-center rounded-full border border-[#E5E5E5] bg-white dark:border-[#262626] dark:bg-[#171717]"
|
||||
>
|
||||
<Ionicons name="chevron-back" size={20} color={isDarkMode ? "#FFFFFF" : "#000000"} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[router, isDarkMode]
|
||||
);
|
||||
|
||||
const renderHeaderRight = () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<View className="flex-row items-center" style={{ gap: Platform.OS === "web" ? 24 : 8 }}>
|
||||
{/* Tab Navigation Dropdown Menu */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<AppPressable
|
||||
className="flex-row items-center gap-1 px-2 py-2"
|
||||
style={{ flexDirection: "row", alignItems: "center" }}
|
||||
const renderHeaderRight = useCallback(
|
||||
() => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<View className="flex-row items-center" style={{ gap: Platform.OS === "web" ? 24 : 8 }}>
|
||||
{/* Tab Navigation Dropdown Menu */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<AppPressable className="h-10 flex-row items-center justify-center gap-2 rounded-full border border-[#E5E5E5] bg-white px-4 dark:border-[#262626] dark:bg-[#171717]">
|
||||
<Text
|
||||
className="text-[15px] font-medium text-[#000000] dark:text-white"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{tabs.find((tab) => tab.id === activeTab)?.label ?? "Basics"}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent
|
||||
insets={{ top: 60, bottom: 20, left: 12, right: 12 }}
|
||||
sideOffset={8}
|
||||
className="w-44"
|
||||
align="end"
|
||||
>
|
||||
<Text className="text-[16px] font-semibold text-[#000000]" numberOfLines={1}>
|
||||
{tabs.find((tab) => tab.id === activeTab)?.label ?? "Basics"}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="chevron-down"
|
||||
size={16}
|
||||
color="#000000"
|
||||
style={{ marginLeft: 2, flexShrink: 0 }}
|
||||
/>
|
||||
</AppPressable>
|
||||
</DropdownMenuTrigger>
|
||||
{tabs.map((tab) => {
|
||||
const isSelected = activeTab === tab.id;
|
||||
return (
|
||||
<DropdownMenuItem key={tab.id} onPress={() => setActiveTab(tab.id)}>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Ionicons
|
||||
name={isSelected ? "checkmark-circle" : tab.icon}
|
||||
size={16}
|
||||
color={isSelected ? (isDarkMode ? "#FFFFFF" : "#000000") : "#666"}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
isSelected
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base text-popover-foreground"
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</View>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<DropdownMenuContent
|
||||
insets={{ top: 60, bottom: 20, left: 12, right: 12 }}
|
||||
sideOffset={8}
|
||||
className="w-44"
|
||||
align="end"
|
||||
>
|
||||
{tabs.map((tab) => {
|
||||
const isSelected = activeTab === tab.id;
|
||||
return (
|
||||
<DropdownMenuItem key={tab.id} onPress={() => setActiveTab(tab.id)}>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Ionicons
|
||||
name={isSelected ? "checkmark-circle" : tab.icon}
|
||||
size={16}
|
||||
color={isSelected ? "#000000" : "#666"}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
isSelected ? "text-base font-semibold text-[#000000]" : "text-base"
|
||||
}
|
||||
>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</View>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* Save Button */}
|
||||
<AppPressable
|
||||
onPress={handleSave}
|
||||
disabled={isSaving || !isDirty}
|
||||
className={`px-2 py-2 ${isSaving || !isDirty ? "opacity-50" : ""}`}
|
||||
>
|
||||
<Text
|
||||
className={`text-[16px] font-semibold ${
|
||||
isSaving || !isDirty ? "text-[#C7C7CC]" : "text-[#000000]"
|
||||
{/* Save Button */}
|
||||
<AppPressable
|
||||
onPress={handleSave}
|
||||
disabled={isSaving || !isDirty}
|
||||
className={`h-10 flex-row items-center justify-center rounded-full border px-5 ${
|
||||
isSaving || !isDirty
|
||||
? "border-[#E5E5EA] bg-[#E5E5EA] dark:border-[#262626] dark:bg-[#262626]"
|
||||
: "border-black bg-black dark:border-white dark:bg-white"
|
||||
}`}
|
||||
>
|
||||
{saveButtonText}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</View>
|
||||
</HeaderButtonWrapper>
|
||||
{isSaving ? (
|
||||
<ActivityIndicator
|
||||
size="small"
|
||||
color={
|
||||
isSaving || !isDirty
|
||||
? isDarkMode
|
||||
? "#A3A3A3"
|
||||
: "#A3A3A3"
|
||||
: isDarkMode
|
||||
? "#000000"
|
||||
: "#FFFFFF"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Text
|
||||
className={`text-[15px] font-medium ${
|
||||
isSaving || !isDirty ? "text-[#A3A3A3]" : isDarkMode ? "text-black" : "text-white"
|
||||
}`}
|
||||
>
|
||||
{saveButtonText}
|
||||
</Text>
|
||||
)}
|
||||
</AppPressable>
|
||||
</View>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[activeTab, isDarkMode, handleSave, isSaving, isDirty, saveButtonText]
|
||||
);
|
||||
|
||||
// Force header update on Android/Web when state changes
|
||||
const navigation = useNavigation();
|
||||
useEffect(() => {
|
||||
if (Platform.OS === "android" || Platform.OS === "web") {
|
||||
navigation.setOptions({
|
||||
headerRight: renderHeaderRight,
|
||||
headerLeft: renderHeaderLeft,
|
||||
});
|
||||
}
|
||||
}, [navigation, renderHeaderRight, renderHeaderLeft]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: headerTitle,
|
||||
headerBackButtonDisplayMode: "minimal",
|
||||
headerLeft:
|
||||
Platform.OS === "android" || Platform.OS === "web" ? renderHeaderLeft : undefined,
|
||||
headerRight:
|
||||
Platform.OS === "android" || Platform.OS === "web" ? renderHeaderRight : undefined,
|
||||
headerShown: Platform.OS !== "ios",
|
||||
headerTransparent: Platform.select({
|
||||
ios: true,
|
||||
}),
|
||||
headerStyle: {
|
||||
backgroundColor: isDarkMode ? "black" : "white",
|
||||
},
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
|
||||
{Platform.OS === "ios" && (
|
||||
<Stack.Header blurEffect={isLiquidGlassAvailable() ? undefined : "light"}>
|
||||
<Stack.Header
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : isDarkMode ? "dark" : "light"}
|
||||
>
|
||||
<Stack.Header.Right>
|
||||
<Stack.Header.Menu>
|
||||
<Stack.Header.Label>
|
||||
@@ -1403,7 +1458,7 @@ export default function EventTypeDetail() {
|
||||
onPress={handleSave}
|
||||
disabled={isSaving || !isDirty}
|
||||
variant="prominent"
|
||||
tintColor="#000"
|
||||
tintColor={isDarkMode ? "#FFF" : "#000"}
|
||||
>
|
||||
{saveButtonText}
|
||||
</Stack.Header.Button>
|
||||
@@ -1411,7 +1466,7 @@ export default function EventTypeDetail() {
|
||||
</Stack.Header>
|
||||
)}
|
||||
|
||||
<View className="flex-1 bg-[#f8f9fa]">
|
||||
<View className="flex-1 bg-[#f8f9fa] dark:bg-black">
|
||||
<ScrollView
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -1467,7 +1522,7 @@ export default function EventTypeDetail() {
|
||||
{isLiquidGlassAvailable() && Platform.OS === "ios" ? (
|
||||
<GlassView glassEffectStyle="regular" className="p-0">
|
||||
<View className="flex-col p-6">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-black">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-black dark:text-white">
|
||||
Select Available Durations
|
||||
</Text>
|
||||
<ScrollView style={{ maxHeight: 400, marginBottom: 20 }}>
|
||||
@@ -1476,37 +1531,43 @@ export default function EventTypeDetail() {
|
||||
key={duration}
|
||||
className={`flex-row items-center justify-between py-3.5 ${
|
||||
index < availableDurations.length - 1
|
||||
? "border-b border-black/10"
|
||||
? "border-b border-black/10 dark:border-white/10"
|
||||
: ""
|
||||
}`}
|
||||
onPress={() => toggleDurationSelection(duration)}
|
||||
>
|
||||
<Text
|
||||
className={`text-[17px] text-black ${
|
||||
className={`text-[17px] text-black dark:text-white ${
|
||||
selectedDurations.includes(duration) ? "font-bold" : "font-medium"
|
||||
}`}
|
||||
>
|
||||
{duration}
|
||||
</Text>
|
||||
{selectedDurations.includes(duration) ? (
|
||||
<Ionicons name="checkmark-circle" size={24} color="#000" />
|
||||
<Ionicons
|
||||
name="checkmark-circle"
|
||||
size={24}
|
||||
color={isDarkMode ? "#FFFFFF" : "#000000"}
|
||||
/>
|
||||
) : (
|
||||
<View className="h-6 w-6 rounded-full border-2 border-black/20" />
|
||||
<View className="h-6 w-6 rounded-full border-2 border-black/20 dark:border-white/20" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
<TouchableOpacity
|
||||
className="items-center rounded-2xl bg-black py-4 active:opacity-80"
|
||||
className="items-center rounded-2xl bg-black py-4 active:opacity-80 dark:bg-white"
|
||||
onPress={() => setShowDurationDropdown(false)}
|
||||
>
|
||||
<Text className="text-[17px] font-bold text-white">Done</Text>
|
||||
<Text className="text-[17px] font-bold text-white dark:text-black">
|
||||
Done
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View className="bg-white p-6">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-[#333]">
|
||||
<View className="bg-white p-6 dark:bg-[#171717]">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-[#333] dark:text-white">
|
||||
Select Available Durations
|
||||
</Text>
|
||||
<ScrollView style={{ maxHeight: 400, marginBottom: 20 }}>
|
||||
@@ -1514,21 +1575,23 @@ export default function EventTypeDetail() {
|
||||
<TouchableOpacity
|
||||
key={duration}
|
||||
className={`flex-row items-center justify-between py-3.5 ${
|
||||
index < availableDurations.length - 1 ? "border-b border-gray-100" : ""
|
||||
index < availableDurations.length - 1
|
||||
? "border-b border-gray-100 dark:border-[#4D4D4D]"
|
||||
: ""
|
||||
}`}
|
||||
onPress={() => toggleDurationSelection(duration)}
|
||||
>
|
||||
<Text
|
||||
className={`text-[17px] text-[#333] ${
|
||||
className={`text-[17px] text-[#333] dark:text-white ${
|
||||
selectedDurations.includes(duration) ? "font-bold" : "font-medium"
|
||||
}`}
|
||||
>
|
||||
{duration}
|
||||
</Text>
|
||||
{selectedDurations.includes(duration) ? (
|
||||
<Ionicons name="checkmark-circle" size={24} color="#000" />
|
||||
<Ionicons name="checkmark-circle" size={24} color="#007AFF" />
|
||||
) : (
|
||||
<View className="h-6 w-6 rounded-full border-2 border-gray-200" />
|
||||
<View className="h-6 w-6 rounded-full border-2 border-gray-200 dark:border-[#4D4D4D]" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -1561,7 +1624,7 @@ export default function EventTypeDetail() {
|
||||
{isLiquidGlassAvailable() && Platform.OS === "ios" ? (
|
||||
<GlassView glassEffectStyle="regular" className="p-0">
|
||||
<View className="flex-col p-6">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-black">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-black dark:text-white">
|
||||
Select Default Duration
|
||||
</Text>
|
||||
<ScrollView style={{ maxHeight: 400 }}>
|
||||
@@ -1569,7 +1632,9 @@ export default function EventTypeDetail() {
|
||||
<TouchableOpacity
|
||||
key={duration}
|
||||
className={`flex-row items-center justify-between py-3.5 ${
|
||||
index < selectedDurations.length - 1 ? "border-b border-black/10" : ""
|
||||
index < selectedDurations.length - 1
|
||||
? "border-b border-black/10 dark:border-white/10"
|
||||
: ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
setDefaultDuration(duration);
|
||||
@@ -1577,14 +1642,18 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-[17px] text-black ${
|
||||
className={`text-[17px] text-black dark:text-white ${
|
||||
defaultDuration === duration ? "font-bold" : "font-medium"
|
||||
}`}
|
||||
>
|
||||
{duration}
|
||||
</Text>
|
||||
{defaultDuration === duration ? (
|
||||
<Ionicons name="checkmark-circle" size={24} color="#000" />
|
||||
<Ionicons
|
||||
name="checkmark-circle"
|
||||
size={24}
|
||||
color={isDarkMode ? "#FFFFFF" : "#000000"}
|
||||
/>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -1592,8 +1661,8 @@ export default function EventTypeDetail() {
|
||||
</View>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View className="bg-white p-6">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-[#333]">
|
||||
<View className="bg-white p-6 dark:bg-[#171717]">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-[#333] dark:text-white">
|
||||
Select Default Duration
|
||||
</Text>
|
||||
<ScrollView style={{ maxHeight: 400 }}>
|
||||
@@ -1601,7 +1670,9 @@ export default function EventTypeDetail() {
|
||||
<TouchableOpacity
|
||||
key={duration}
|
||||
className={`flex-row items-center justify-between py-3.5 ${
|
||||
index < selectedDurations.length - 1 ? "border-b border-gray-100" : ""
|
||||
index < selectedDurations.length - 1
|
||||
? "border-b border-gray-100 dark:border-[#4D4D4D]"
|
||||
: ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
setDefaultDuration(duration);
|
||||
@@ -1609,14 +1680,14 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-[17px] text-[#333] ${
|
||||
className={`text-[17px] text-[#333] dark:text-white ${
|
||||
defaultDuration === duration ? "font-bold" : "font-medium"
|
||||
}`}
|
||||
>
|
||||
{duration}
|
||||
</Text>
|
||||
{defaultDuration === duration ? (
|
||||
<Ionicons name="checkmark-circle" size={24} color="#000" />
|
||||
<Ionicons name="checkmark-circle" size={24} color="#007AFF" />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -1643,7 +1714,7 @@ export default function EventTypeDetail() {
|
||||
{isLiquidGlassAvailable() && Platform.OS === "ios" ? (
|
||||
<GlassView glassEffectStyle="regular" className="p-0">
|
||||
<View className="flex-col p-6">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-black">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-black dark:text-white">
|
||||
Select Schedule
|
||||
</Text>
|
||||
<ScrollView style={{ maxHeight: 400 }}>
|
||||
@@ -1651,7 +1722,9 @@ export default function EventTypeDetail() {
|
||||
<TouchableOpacity
|
||||
key={schedule.id}
|
||||
className={`flex-row items-center justify-between py-3.5 ${
|
||||
index < schedules.length - 1 ? "border-b border-black/10" : ""
|
||||
index < schedules.length - 1
|
||||
? "border-b border-black/10 dark:border-white/10"
|
||||
: ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
setSelectedSchedule(schedule);
|
||||
@@ -1661,18 +1734,24 @@ export default function EventTypeDetail() {
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className={`text-[17px] text-black ${
|
||||
className={`text-[17px] text-black dark:text-white ${
|
||||
selectedSchedule?.id === schedule.id ? "font-bold" : "font-medium"
|
||||
}`}
|
||||
>
|
||||
{schedule.name}
|
||||
</Text>
|
||||
{schedule.isDefault && (
|
||||
<Text className="text-[13px] text-black/50">Default</Text>
|
||||
<Text className="text-[13px] text-black/50 dark:text-white/50">
|
||||
Default
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{selectedSchedule?.id === schedule.id ? (
|
||||
<Ionicons name="checkmark-circle" size={24} color="#000" />
|
||||
<Ionicons
|
||||
name="checkmark-circle"
|
||||
size={24}
|
||||
color={isDarkMode ? "#FFFFFF" : "#000000"}
|
||||
/>
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -1680,8 +1759,8 @@ export default function EventTypeDetail() {
|
||||
</View>
|
||||
</GlassView>
|
||||
) : (
|
||||
<View className="bg-white p-6">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-[#333]">
|
||||
<View className="bg-white p-6 dark:bg-[#171717]">
|
||||
<Text className="mb-5 text-center text-[19px] font-bold text-[#333] dark:text-white">
|
||||
Select Schedule
|
||||
</Text>
|
||||
<ScrollView style={{ maxHeight: 400 }}>
|
||||
@@ -1689,7 +1768,9 @@ export default function EventTypeDetail() {
|
||||
<TouchableOpacity
|
||||
key={schedule.id}
|
||||
className={`flex-row items-center justify-between py-3.5 ${
|
||||
index < schedules.length - 1 ? "border-b border-gray-100" : ""
|
||||
index < schedules.length - 1
|
||||
? "border-b border-gray-100 dark:border-[#4D4D4D]"
|
||||
: ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
setSelectedSchedule(schedule);
|
||||
@@ -1699,18 +1780,20 @@ export default function EventTypeDetail() {
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className={`text-[17px] text-[#333] ${
|
||||
className={`text-[17px] text-[#333] dark:text-white ${
|
||||
selectedSchedule?.id === schedule.id ? "font-bold" : "font-medium"
|
||||
}`}
|
||||
>
|
||||
{schedule.name}
|
||||
</Text>
|
||||
{schedule.isDefault && (
|
||||
<Text className="text-[13px] text-gray-500">Default</Text>
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">
|
||||
Default
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
{selectedSchedule?.id === schedule.id ? (
|
||||
<Ionicons name="checkmark-circle" size={24} color="#000" />
|
||||
<Ionicons name="checkmark-circle" size={24} color="#007AFF" />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -1733,10 +1816,10 @@ export default function EventTypeDetail() {
|
||||
onPress={() => setShowTimezoneDropdown(false)}
|
||||
>
|
||||
<View
|
||||
className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5"
|
||||
className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5 dark:bg-[#171717]"
|
||||
style={{ maxHeight: "70%" }}
|
||||
>
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333]">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333] dark:text-white">
|
||||
Select Timezone
|
||||
</Text>
|
||||
<ScrollView style={{ maxHeight: 400 }}>
|
||||
@@ -1759,8 +1842,8 @@ export default function EventTypeDetail() {
|
||||
className={`mb-1 flex-row items-center justify-between rounded-lg px-2 py-3 md:px-4 ${
|
||||
selectedTimezone === tz ||
|
||||
(selectedScheduleDetails?.timeZone === tz && !selectedTimezone)
|
||||
? "bg-[#F0F0F0]"
|
||||
: "active:bg-[#F0F0F0]"
|
||||
? "bg-[#F0F0F0] dark:bg-[#262626]"
|
||||
: "active:bg-[#F0F0F0] dark:active:bg-[#262626]"
|
||||
}`}
|
||||
onPress={() => {
|
||||
setSelectedTimezone(tz);
|
||||
@@ -1768,7 +1851,7 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-base text-[#333] ${
|
||||
className={`text-base text-[#333] dark:text-white ${
|
||||
selectedTimezone === tz ||
|
||||
(selectedScheduleDetails?.timeZone === tz && !selectedTimezone)
|
||||
? "font-semibold"
|
||||
@@ -1779,7 +1862,7 @@ export default function EventTypeDetail() {
|
||||
</Text>
|
||||
{selectedTimezone === tz ||
|
||||
(selectedScheduleDetails?.timeZone === tz && !selectedTimezone) ? (
|
||||
<Ionicons name="checkmark" size={20} color="#000" />
|
||||
<Ionicons name="checkmark" size={20} color="#007AFF" />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -1799,15 +1882,15 @@ export default function EventTypeDetail() {
|
||||
className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]"
|
||||
onPress={() => setShowBeforeBufferDropdown(false)}
|
||||
>
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333]">
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5 dark:bg-[#171717]">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333] dark:text-white">
|
||||
Before event buffer
|
||||
</Text>
|
||||
{bufferTimeOptions.map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option}
|
||||
className={`mb-1 flex-row items-center justify-between rounded-lg px-4 py-3 ${
|
||||
beforeEventBuffer === option ? "bg-[#F0F0F0]" : ""
|
||||
beforeEventBuffer === option ? "bg-[#F0F0F0] dark:bg-[#262626]" : ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
setBeforeEventBuffer(option);
|
||||
@@ -1815,14 +1898,14 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-base text-[#333] ${
|
||||
className={`text-base text-[#333] dark:text-white ${
|
||||
beforeEventBuffer === option ? "font-semibold" : ""
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</Text>
|
||||
{beforeEventBuffer === option ? (
|
||||
<Ionicons name="checkmark" size={20} color="#000" />
|
||||
<Ionicons name="checkmark" size={20} color="#007AFF" />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -1841,15 +1924,15 @@ export default function EventTypeDetail() {
|
||||
className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]"
|
||||
onPress={() => setShowAfterBufferDropdown(false)}
|
||||
>
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333]">
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5 dark:bg-[#171717]">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333] dark:text-white">
|
||||
After event buffer
|
||||
</Text>
|
||||
{bufferTimeOptions.map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option}
|
||||
className={`mb-1 flex-row items-center justify-between rounded-lg px-4 py-3 ${
|
||||
afterEventBuffer === option ? "bg-[#F0F0F0]" : ""
|
||||
afterEventBuffer === option ? "bg-[#F0F0F0] dark:bg-[#262626]" : ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
setAfterEventBuffer(option);
|
||||
@@ -1857,14 +1940,14 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-base text-[#333] ${
|
||||
className={`text-base text-[#333] dark:text-white ${
|
||||
afterEventBuffer === option ? "font-semibold" : ""
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</Text>
|
||||
{afterEventBuffer === option ? (
|
||||
<Ionicons name="checkmark" size={20} color="#000" />
|
||||
<Ionicons name="checkmark" size={20} color="#007AFF" />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -1883,15 +1966,15 @@ export default function EventTypeDetail() {
|
||||
className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]"
|
||||
onPress={() => setShowMinimumNoticeUnitDropdown(false)}
|
||||
>
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333]">
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5 dark:bg-[#171717]">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333] dark:text-white">
|
||||
Time unit
|
||||
</Text>
|
||||
{timeUnitOptions.map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option}
|
||||
className={`mb-1 flex-row items-center justify-between rounded-lg px-4 py-3 ${
|
||||
minimumNoticeUnit === option ? "bg-[#F0F0F0]" : ""
|
||||
minimumNoticeUnit === option ? "bg-[#F0F0F0] dark:bg-[#262626]" : ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
setMinimumNoticeUnit(option);
|
||||
@@ -1899,14 +1982,14 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-base text-[#333] ${
|
||||
className={`text-base text-[#333] dark:text-white ${
|
||||
minimumNoticeUnit === option ? "font-semibold" : ""
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</Text>
|
||||
{minimumNoticeUnit === option ? (
|
||||
<Ionicons name="checkmark" size={20} color="#000" />
|
||||
<Ionicons name="checkmark" size={20} color="#007AFF" />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -1925,8 +2008,8 @@ export default function EventTypeDetail() {
|
||||
className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]"
|
||||
onPress={() => setShowFrequencyUnitDropdown(null)}
|
||||
>
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333]">
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5 dark:bg-[#171717]">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333] dark:text-white">
|
||||
Frequency unit
|
||||
</Text>
|
||||
{frequencyUnitOptions.map((option) => {
|
||||
@@ -1938,7 +2021,7 @@ export default function EventTypeDetail() {
|
||||
<TouchableOpacity
|
||||
key={option}
|
||||
className={`mb-1 flex-row items-center justify-between rounded-lg px-2 py-3 md:px-4 ${
|
||||
isSelected ? "bg-[#F0F0F0]" : ""
|
||||
isSelected ? "bg-[#F0F0F0] dark:bg-[#262626]" : ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
if (showFrequencyUnitDropdown) {
|
||||
@@ -1948,11 +2031,11 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-base text-[#333] ${isSelected ? "font-semibold" : ""}`}
|
||||
className={`text-base text-[#333] dark:text-white ${isSelected ? "font-semibold" : ""}`}
|
||||
>
|
||||
{option}
|
||||
</Text>
|
||||
{isSelected && <Ionicons name="checkmark" size={20} color="#000" />}
|
||||
{isSelected && <Ionicons name="checkmark" size={20} color="#007AFF" />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
@@ -1971,8 +2054,8 @@ export default function EventTypeDetail() {
|
||||
className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]"
|
||||
onPress={() => setShowDurationUnitDropdown(null)}
|
||||
>
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333]">
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5 dark:bg-[#171717]">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333] dark:text-white">
|
||||
Duration unit
|
||||
</Text>
|
||||
{durationUnitOptions.map((option) => {
|
||||
@@ -1984,7 +2067,7 @@ export default function EventTypeDetail() {
|
||||
<TouchableOpacity
|
||||
key={option}
|
||||
className={`mb-1 flex-row items-center justify-between rounded-lg px-2 py-3 md:px-4 ${
|
||||
isSelected ? "bg-[#F0F0F0]" : ""
|
||||
isSelected ? "bg-[#F0F0F0] dark:bg-[#262626]" : ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
if (showDurationUnitDropdown) {
|
||||
@@ -1994,11 +2077,11 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-base text-[#333] ${isSelected ? "font-semibold" : ""}`}
|
||||
className={`text-base text-[#333] dark:text-white ${isSelected ? "font-semibold" : ""}`}
|
||||
>
|
||||
{option}
|
||||
</Text>
|
||||
{isSelected && <Ionicons name="checkmark" size={20} color="#000" />}
|
||||
{isSelected && <Ionicons name="checkmark" size={20} color="#007AFF" />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
@@ -2017,15 +2100,15 @@ export default function EventTypeDetail() {
|
||||
className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]"
|
||||
onPress={() => setShowSlotIntervalDropdown(false)}
|
||||
>
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333]">
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5 dark:bg-[#171717]">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333] dark:text-white">
|
||||
Slot interval
|
||||
</Text>
|
||||
{slotIntervalOptions.map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option}
|
||||
className={`mb-1 flex-row items-center justify-between rounded-lg px-4 py-3 ${
|
||||
slotInterval === option ? "bg-[#F0F0F0]" : ""
|
||||
slotInterval === option ? "bg-[#F0F0F0] dark:bg-[#262626]" : ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
setSlotInterval(option);
|
||||
@@ -2033,14 +2116,14 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-base text-[#333] ${
|
||||
className={`text-base text-[#333] dark:text-white ${
|
||||
slotInterval === option ? "font-semibold" : ""
|
||||
}`}
|
||||
>
|
||||
{option}
|
||||
</Text>
|
||||
{slotInterval === option ? (
|
||||
<Ionicons name="checkmark" size={20} color="#000" />
|
||||
<Ionicons name="checkmark" size={20} color="#007AFF" />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -2059,15 +2142,15 @@ export default function EventTypeDetail() {
|
||||
className="flex-1 items-center justify-center bg-[rgba(0,0,0,0.5)]"
|
||||
onPress={() => setShowRecurringFrequencyDropdown(false)}
|
||||
>
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333]">
|
||||
<View className="min-w-[250px] max-w-[80%] rounded-2xl bg-white p-5 dark:bg-[#171717]">
|
||||
<Text className="mb-4 text-center text-lg font-semibold text-[#333] dark:text-white">
|
||||
Repeats every
|
||||
</Text>
|
||||
{(["weekly", "monthly", "yearly"] as const).map((option) => (
|
||||
<TouchableOpacity
|
||||
key={option}
|
||||
className={`mb-1 flex-row items-center justify-between rounded-lg px-4 py-3 ${
|
||||
recurringFrequency === option ? "bg-[#F0F0F0]" : ""
|
||||
recurringFrequency === option ? "bg-[#F0F0F0] dark:bg-[#262626]" : ""
|
||||
}`}
|
||||
onPress={() => {
|
||||
setRecurringFrequency(option);
|
||||
@@ -2075,14 +2158,14 @@ export default function EventTypeDetail() {
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className={`text-base capitalize text-[#333] ${
|
||||
className={`text-base capitalize text-[#333] dark:text-white ${
|
||||
recurringFrequency === option ? "font-semibold" : ""
|
||||
}`}
|
||||
>
|
||||
{option === "weekly" ? "week" : option === "monthly" ? "month" : "year"}
|
||||
</Text>
|
||||
{recurringFrequency === option ? (
|
||||
<Ionicons name="checkmark" size={20} color="#000" />
|
||||
<Ionicons name="checkmark" size={20} color="#007AFF" />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
@@ -2238,16 +2321,16 @@ export default function EventTypeDetail() {
|
||||
{/* Configure on Web */}
|
||||
<View>
|
||||
<Text
|
||||
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72]"
|
||||
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72] dark:text-[#A3A3A3]"
|
||||
style={{ letterSpacing: 0.5 }}
|
||||
>
|
||||
Configure on Web
|
||||
</Text>
|
||||
<View className="overflow-hidden rounded-[10px] bg-white">
|
||||
<View className="overflow-hidden rounded-[10px] bg-white dark:bg-[#171717]">
|
||||
{/* Apps */}
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4 dark:bg-[#171717]">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4 dark:border-[#4D4D4D]"
|
||||
style={{ minHeight: 56 }}
|
||||
onPress={() => {
|
||||
if (id === "new") {
|
||||
@@ -2267,12 +2350,12 @@ export default function EventTypeDetail() {
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
<View className="flex-row items-center py-2">
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7]">
|
||||
<Ionicons name="grid" size={18} color="#000000" />
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7] dark:bg-[#262626]">
|
||||
<Ionicons name="grid" size={18} color="#A3A3A3" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">Apps</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Manage integrations</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">Apps</Text>
|
||||
<Text className="text-[13px] text-[#A3A3A3]">Manage integrations</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons name="open-outline" size={18} color="#C7C7CC" />
|
||||
@@ -2280,9 +2363,9 @@ export default function EventTypeDetail() {
|
||||
</View>
|
||||
|
||||
{/* Workflows */}
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4 dark:bg-[#171717]">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4 dark:border-[#4D4D4D]"
|
||||
style={{ minHeight: 56 }}
|
||||
onPress={() => {
|
||||
if (id === "new") {
|
||||
@@ -2302,12 +2385,12 @@ export default function EventTypeDetail() {
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
<View className="flex-row items-center py-2">
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7]">
|
||||
<Ionicons name="flash" size={18} color="#000000" />
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7] dark:bg-[#262626]">
|
||||
<Ionicons name="flash" size={18} color="#A3A3A3" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">Workflows</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Automated actions</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">Workflows</Text>
|
||||
<Text className="text-[13px] text-[#A3A3A3]">Automated actions</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons name="open-outline" size={18} color="#C7C7CC" />
|
||||
@@ -2315,7 +2398,7 @@ export default function EventTypeDetail() {
|
||||
</View>
|
||||
|
||||
{/* Webhooks */}
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4 dark:bg-[#171717]">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-between pr-4"
|
||||
style={{ minHeight: 56 }}
|
||||
@@ -2337,12 +2420,12 @@ export default function EventTypeDetail() {
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
<View className="flex-row items-center py-2">
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7]">
|
||||
<Ionicons name="code" size={18} color="#000000" />
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7] dark:bg-[#262626]">
|
||||
<Ionicons name="code" size={18} color="#A3A3A3" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">Webhooks</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Event notifications</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">Webhooks</Text>
|
||||
<Text className="text-[13px] text-[#A3A3A3]">Event notifications</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons name="open-outline" size={18} color="#C7C7CC" />
|
||||
@@ -2358,23 +2441,26 @@ export default function EventTypeDetail() {
|
||||
{/* Visibility */}
|
||||
<View>
|
||||
<Text
|
||||
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72]"
|
||||
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72] dark:text-[#A3A3A3]"
|
||||
style={{ letterSpacing: 0.5 }}
|
||||
>
|
||||
Visibility
|
||||
</Text>
|
||||
<View className="overflow-hidden rounded-[10px] bg-white">
|
||||
<View className="bg-white pl-4">
|
||||
<View className="overflow-hidden rounded-[10px] bg-white dark:bg-[#171717]">
|
||||
<View className="bg-white pl-4 dark:bg-[#171717]">
|
||||
<View
|
||||
className="flex-row items-center justify-between pr-4"
|
||||
style={{ height: 44, flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Hidden</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">Hidden</Text>
|
||||
<View style={{ justifyContent: "center", height: "100%" }}>
|
||||
<Switch
|
||||
value={isHidden}
|
||||
onValueChange={setIsHidden}
|
||||
trackColor={{ false: "#E5E5EA", true: "#000000" }}
|
||||
trackColor={{
|
||||
false: isDarkMode ? "#404040" : "#E5E5EA",
|
||||
true: isDarkMode ? "#34C759" : "#000000",
|
||||
}}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
@@ -2386,33 +2472,33 @@ export default function EventTypeDetail() {
|
||||
{/* Quick Actions */}
|
||||
<View>
|
||||
<Text
|
||||
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72]"
|
||||
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72] dark:text-[#A3A3A3]"
|
||||
style={{ letterSpacing: 0.5 }}
|
||||
>
|
||||
Quick Actions
|
||||
</Text>
|
||||
<View className="overflow-hidden rounded-[10px] bg-white">
|
||||
<View className="bg-white pl-4">
|
||||
<View className="overflow-hidden rounded-[10px] bg-white dark:bg-[#171717]">
|
||||
<View className="bg-white pl-4 dark:bg-[#171717]">
|
||||
{Platform.OS !== "ios" && (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4 dark:border-[#4D4D4D]"
|
||||
style={{ height: 44 }}
|
||||
onPress={handlePreview}
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Preview</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">Preview</Text>
|
||||
<Ionicons name="open-outline" size={18} color="#C7C7CC" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4 dark:bg-[#171717]">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-between pr-4"
|
||||
style={{ height: 44 }}
|
||||
onPress={handleCopyLink}
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Copy Link</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">Copy Link</Text>
|
||||
<Ionicons name="link-outline" size={18} color="#C7C7CC" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -2421,8 +2507,8 @@ export default function EventTypeDetail() {
|
||||
|
||||
{/* Danger Zone */}
|
||||
<View>
|
||||
<View className="overflow-hidden rounded-[10px] bg-white">
|
||||
<View className="bg-white pl-4">
|
||||
<View className="overflow-hidden rounded-[10px] bg-white dark:bg-[#171717]">
|
||||
<View className="bg-white pl-4 dark:bg-[#171717]">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-between pr-4"
|
||||
style={{ height: 44 }}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Button, Host, Image as SwiftUIImage } from "@expo/ui/swift-ui";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { buttonStyle, controlSize, frame, padding } from "@expo/ui/swift-ui/modifiers";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
@@ -15,6 +13,7 @@ import {
|
||||
Share,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { EmptyScreen } from "@/components/EmptyScreen";
|
||||
@@ -36,10 +35,15 @@ import { getEventDuration } from "@/utils/getEventDuration";
|
||||
import { offlineAwareRefresh } from "@/utils/network";
|
||||
import { slugify } from "@/utils/slugify";
|
||||
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
export default function EventTypesIOS() {
|
||||
const router = useRouter();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
// No modal state needed for iOS - using native Alert.prompt
|
||||
|
||||
@@ -337,12 +341,12 @@ export default function EventTypesIOS() {
|
||||
<>
|
||||
<Stack.Header
|
||||
style={{ backgroundColor: "transparent", shadowColor: "transparent" }}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : "light"}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : isDark ? "dark" : "light"}
|
||||
>
|
||||
<Stack.Header.Title large>Event Types</Stack.Header.Title>
|
||||
</Stack.Header>
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: theme.background }}
|
||||
contentContainerStyle={{ paddingBottom: 120, paddingTop: 16 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
@@ -355,14 +359,34 @@ export default function EventTypesIOS() {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-gray-50 p-5"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<Ionicons name="alert-circle" size={64} color="#FF3B30" />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-gray-800">
|
||||
<Text
|
||||
className="mb-2 mt-4 text-center text-xl font-bold text-gray-800"
|
||||
style={{ color: theme.text }}
|
||||
>
|
||||
Unable to load event types
|
||||
</Text>
|
||||
<Text className="mb-6 text-center text-base text-gray-500">{error}</Text>
|
||||
<TouchableOpacity className="rounded-lg bg-black px-6 py-3" onPress={() => refetch()}>
|
||||
<Text className="text-base font-semibold text-white">Retry</Text>
|
||||
<Text
|
||||
className="mb-6 text-center text-base text-gray-500"
|
||||
style={{ color: theme.textMuted }}
|
||||
>
|
||||
{error}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
className="rounded-lg bg-black px-6 py-3"
|
||||
style={{ backgroundColor: isDark ? "white" : "black" }}
|
||||
onPress={() => refetch()}
|
||||
>
|
||||
<Text
|
||||
className="text-base font-semibold text-white"
|
||||
style={{ color: isDark ? "black" : "white" }}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
@@ -374,12 +398,15 @@ export default function EventTypesIOS() {
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Event Types",
|
||||
headerBlurEffect: isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect: isLiquidGlassAvailable() ? undefined : isDark ? "dark" : "light",
|
||||
headerStyle: { backgroundColor: "transparent" },
|
||||
headerLargeTitleEnabled: true,
|
||||
}}
|
||||
/>
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-gray-50 p-5"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<EmptyScreen
|
||||
icon="link-outline"
|
||||
headline="Create your first event type"
|
||||
@@ -397,7 +424,7 @@ export default function EventTypesIOS() {
|
||||
{/* iOS Native Header with Glass UI */}
|
||||
<Stack.Header
|
||||
style={{ backgroundColor: "transparent", shadowColor: "transparent" }}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : "light"}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : isDark ? "dark" : "light"}
|
||||
>
|
||||
<Stack.Header.Title large>Event Types</Stack.Header.Title>
|
||||
|
||||
@@ -494,13 +521,13 @@ export default function EventTypesIOS() {
|
||||
placeholder="Search event types"
|
||||
onChangeText={(e) => handleSearch(e.nativeEvent.text)}
|
||||
obscureBackground={false}
|
||||
barTintColor="#fff"
|
||||
barTintColor={isDark ? "#171717" : "#fff"}
|
||||
/>
|
||||
</Stack.Header>
|
||||
|
||||
{/* Event Types List */}
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: theme.background }}
|
||||
contentContainerStyle={{ paddingBottom: 120 }}
|
||||
refreshControl={<RefreshControl refreshing={false} onRefresh={onRefresh} />}
|
||||
showsVerticalScrollIndicator={false}
|
||||
@@ -509,7 +536,10 @@ export default function EventTypesIOS() {
|
||||
{refreshing ? (
|
||||
<EventTypeListSkeleton />
|
||||
) : filteredEventTypes.length === 0 && searchQuery.trim() !== "" ? (
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5 pt-20">
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-gray-50 p-5 pt-20"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<EmptyScreen
|
||||
icon="search-outline"
|
||||
headline={`No results found for "${searchQuery}"`}
|
||||
@@ -517,7 +547,10 @@ export default function EventTypesIOS() {
|
||||
/>
|
||||
</View>
|
||||
) : filteredEventTypes.length === 0 && activeFilterCount > 0 ? (
|
||||
<View className="flex-1 items-center justify-center bg-white p-5 pt-20">
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-white p-5 pt-20"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
>
|
||||
<EmptyScreen
|
||||
icon="filter-outline"
|
||||
headline="No event types match your filters"
|
||||
@@ -529,7 +562,14 @@ export default function EventTypesIOS() {
|
||||
</View>
|
||||
) : (
|
||||
<View className="px-2 pt-4 md:px-4">
|
||||
<View className="overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
className="overflow-hidden rounded-lg"
|
||||
style={{
|
||||
backgroundColor: theme.backgroundSecondary,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.border,
|
||||
}}
|
||||
>
|
||||
{filteredEventTypes.map((item, index) => (
|
||||
<EventTypeListItem
|
||||
key={item.id.toString()}
|
||||
@@ -551,23 +591,13 @@ export default function EventTypesIOS() {
|
||||
|
||||
{/* Floating Action Button for New Event Type with Glass UI Menu */}
|
||||
<View className="absolute right-6" style={{ bottom: 100 }}>
|
||||
<Host matchContents>
|
||||
<Button
|
||||
onPress={handleOpenCreateModal}
|
||||
modifiers={[
|
||||
buttonStyle(isLiquidGlassAvailable() ? "glass" : "bordered"),
|
||||
padding(),
|
||||
controlSize("large"),
|
||||
]}
|
||||
>
|
||||
<SwiftUIImage
|
||||
systemName="plus"
|
||||
color="primary"
|
||||
size={24}
|
||||
modifiers={[frame({ height: 24, width: 17 })]}
|
||||
/>
|
||||
</Button>
|
||||
</Host>
|
||||
<TouchableOpacity
|
||||
onPress={handleOpenCreateModal}
|
||||
className="h-14 w-14 items-center justify-center rounded-full bg-black shadow-lg shadow-black/20 dark:bg-white"
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Ionicons name="add" size={30} color={isDark ? "#000000" : "#FFFFFF"} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { EmptyScreen } from "@/components/EmptyScreen";
|
||||
@@ -46,9 +47,14 @@ import { normalizeMarkdown } from "@/utils/normalizeMarkdown";
|
||||
import { shadows } from "@/utils/shadows";
|
||||
import { slugify } from "@/utils/slugify";
|
||||
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
export default function EventTypes() {
|
||||
const router = useRouter();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
// Modal state for creating new event type
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
@@ -381,10 +387,10 @@ export default function EventTypes() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-1" style={{ backgroundColor: theme.background }}>
|
||||
{Platform.OS === "web" && <Header />}
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: theme.background }}
|
||||
contentContainerStyle={{ paddingBottom: 90 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
@@ -397,16 +403,36 @@ export default function EventTypes() {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<View className="flex-1" style={{ backgroundColor: theme.background }}>
|
||||
{Platform.OS === "web" && <Header />}
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<Ionicons name="alert-circle" size={64} color="#800020" />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-gray-800">
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-gray-50 p-5"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<Ionicons name="alert-circle" size={64} color={theme.destructive} />
|
||||
<Text
|
||||
className="mb-2 mt-4 text-center text-xl font-bold text-gray-800"
|
||||
style={{ color: theme.text }}
|
||||
>
|
||||
Unable to load event types
|
||||
</Text>
|
||||
<Text className="mb-6 text-center text-base text-gray-500">{error}</Text>
|
||||
<TouchableOpacity className="rounded-lg bg-black px-6 py-3" onPress={() => refetch()}>
|
||||
<Text className="text-base font-semibold text-white">Retry</Text>
|
||||
<Text
|
||||
className="mb-6 text-center text-base text-gray-500"
|
||||
style={{ color: theme.textMuted }}
|
||||
>
|
||||
{error}
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
className="rounded-lg bg-black px-6 py-3"
|
||||
style={{ backgroundColor: isDark ? "white" : "black" }}
|
||||
onPress={() => refetch()}
|
||||
>
|
||||
<Text
|
||||
className="text-base font-semibold text-white"
|
||||
style={{ color: isDark ? "black" : "white" }}
|
||||
>
|
||||
Retry
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -415,9 +441,12 @@ export default function EventTypes() {
|
||||
|
||||
if (eventTypes.length === 0) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<View className="flex-1" style={{ backgroundColor: theme.background }}>
|
||||
{Platform.OS === "web" && <Header />}
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-gray-50 p-5"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<EmptyScreen
|
||||
icon="link-outline"
|
||||
headline="Create your first event type"
|
||||
@@ -432,15 +461,26 @@ export default function EventTypes() {
|
||||
|
||||
if (filteredEventTypes.length === 0 && searchQuery.trim() !== "") {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<View className="flex-1" style={{ backgroundColor: theme.background }}>
|
||||
{Platform.OS === "web" && (
|
||||
<>
|
||||
<Header />
|
||||
<View className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2">
|
||||
<View
|
||||
className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2"
|
||||
style={{
|
||||
borderBottomColor: theme.border,
|
||||
backgroundColor: theme.backgroundSecondary,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-[17px] text-black focus:border-black focus:ring-2 focus:ring-black"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: isDark ? theme.background : "white",
|
||||
color: theme.text,
|
||||
}}
|
||||
placeholder="Search event types"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={theme.textSecondary}
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
autoCapitalize="none"
|
||||
@@ -449,16 +489,29 @@ export default function EventTypes() {
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
|
||||
style={{ flexDirection: "row", alignItems: "center", justifyContent: "center" }}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: isDark ? "white" : "black",
|
||||
}}
|
||||
onPress={handleCreateNew}
|
||||
>
|
||||
<Ionicons name="add" size={18} color="#fff" />
|
||||
<Text className="text-base font-semibold text-white">New</Text>
|
||||
<Ionicons name="add" size={18} color={isDark ? "#000" : "#fff"} />
|
||||
<Text
|
||||
className="text-base font-semibold text-white"
|
||||
style={{ color: isDark ? "black" : "white" }}
|
||||
>
|
||||
New
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-gray-50 p-5"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<EmptyScreen
|
||||
icon="search-outline"
|
||||
headline={`No results found for "${searchQuery}"`}
|
||||
@@ -501,11 +554,19 @@ export default function EventTypes() {
|
||||
activeFilterCount,
|
||||
}}
|
||||
/>
|
||||
<View className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2">
|
||||
<View
|
||||
className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2"
|
||||
style={{ borderBottomColor: theme.border, backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<TextInput
|
||||
className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-[17px] text-black focus:border-black focus:ring-2 focus:ring-black"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: isDark ? theme.background : "white",
|
||||
color: theme.text,
|
||||
}}
|
||||
placeholder="Search event types"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={theme.textSecondary}
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
autoCapitalize="none"
|
||||
@@ -514,18 +575,28 @@ export default function EventTypes() {
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
|
||||
style={{ flexDirection: "row", alignItems: "center", justifyContent: "center" }}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: isDark ? "white" : "black",
|
||||
}}
|
||||
onPress={handleCreateNew}
|
||||
>
|
||||
<Ionicons name="add" size={18} color="#fff" />
|
||||
<Text className="text-base font-semibold text-white">New</Text>
|
||||
<Ionicons name="add" size={18} color={isDark ? "#000" : "#fff"} />
|
||||
<Text
|
||||
className="text-base font-semibold text-white"
|
||||
style={{ color: isDark ? "black" : "white" }}
|
||||
>
|
||||
New
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: theme.background }}
|
||||
contentContainerStyle={{ paddingBottom: 90 }}
|
||||
refreshControl={<RefreshControl refreshing={false} onRefresh={onRefresh} />}
|
||||
showsVerticalScrollIndicator={false}
|
||||
@@ -534,7 +605,10 @@ export default function EventTypes() {
|
||||
{refreshing ? (
|
||||
<EventTypeListSkeleton />
|
||||
) : filteredEventTypes.length === 0 && activeFilterCount > 0 ? (
|
||||
<View className="flex-1 items-center justify-center bg-white p-5 pt-20">
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-white p-5 pt-20"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
>
|
||||
<EmptyScreen
|
||||
icon="filter-outline"
|
||||
headline="No event types match your filters"
|
||||
@@ -546,7 +620,10 @@ export default function EventTypes() {
|
||||
</View>
|
||||
) : (
|
||||
<View className="px-2 pt-4 md:px-4">
|
||||
<View className="overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
className="overflow-hidden rounded-lg border border-[#E5E5EA] bg-white"
|
||||
style={{ borderColor: theme.border, backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
{filteredEventTypes.map((item, index) => (
|
||||
<EventTypeListItem
|
||||
key={item.id.toString()}
|
||||
@@ -639,14 +716,17 @@ export default function EventTypes() {
|
||||
className="max-h-[90%] w-[90%] max-w-[500px] rounded-2xl bg-white"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={shadows.xl()}
|
||||
style={{ ...shadows.xl(), backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="px-8 pb-4 pt-6">
|
||||
<Text className="mb-2 text-2xl font-semibold text-gray-900">
|
||||
<Text
|
||||
className="mb-2 text-2xl font-semibold text-gray-900"
|
||||
style={{ color: theme.text }}
|
||||
>
|
||||
Add a new event type
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">
|
||||
<Text className="text-sm text-gray-500" style={{ color: theme.textSecondary }}>
|
||||
Set up event types to offer different types of meetings.
|
||||
</Text>
|
||||
</View>
|
||||
@@ -655,11 +735,21 @@ export default function EventTypes() {
|
||||
<View className="px-8 pb-6">
|
||||
{/* Title */}
|
||||
<View className="mb-4">
|
||||
<Text className="mb-2 text-sm font-medium text-gray-700">Title</Text>
|
||||
<Text
|
||||
className="mb-2 text-sm font-medium text-gray-700"
|
||||
style={{ color: theme.textSecondary }}
|
||||
>
|
||||
Title
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-md border border-gray-300 bg-white px-3 py-2.5 text-base text-gray-900 focus:border-black focus:ring-2 focus:ring-black"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: isDark ? theme.backgroundMuted : "white",
|
||||
color: theme.text,
|
||||
}}
|
||||
placeholder="Quick Chat"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={theme.textSecondary}
|
||||
value={newEventTitle}
|
||||
onChangeText={setNewEventTitle}
|
||||
autoFocus
|
||||
@@ -671,21 +761,25 @@ export default function EventTypes() {
|
||||
</View>
|
||||
|
||||
{/* Footer */}
|
||||
<View className="rounded-b-2xl border-t border-[#E5E7EB] bg-[#F9FAFB] px-8 py-4">
|
||||
<View className="rounded-b-2xl border-t border-[#E5E7EB] bg-[#F9FAFB] px-8 py-4 dark:border-[#4D4D4D] dark:bg-[#2C2C2E]">
|
||||
<View className="flex-row justify-end gap-2 space-x-2">
|
||||
<TouchableOpacity
|
||||
className="rounded-xl border border-gray-300 bg-white px-4 py-2"
|
||||
className="rounded-xl border border-gray-300 bg-white px-4 py-2 dark:border-[#4D4D4D] dark:bg-[#171717]"
|
||||
onPress={handleCloseCreateModal}
|
||||
disabled={creating}
|
||||
>
|
||||
<Text className="text-base font-medium text-gray-700">Close</Text>
|
||||
<Text className="text-base font-medium text-gray-700 dark:text-gray-300">
|
||||
Close
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className={`rounded-xl bg-[#111827] px-4 py-2 ${creating ? "opacity-60" : ""}`}
|
||||
className={`rounded-xl bg-[#111827] px-4 py-2 dark:bg-white ${creating ? "opacity-60" : ""}`}
|
||||
onPress={handleCreateEventType}
|
||||
disabled={creating}
|
||||
>
|
||||
<Text className="text-base font-medium text-white">Continue</Text>
|
||||
<Text className="text-base font-medium text-white dark:text-black">
|
||||
Continue
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -712,18 +806,18 @@ export default function EventTypes() {
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="mx-4 w-full max-w-sm rounded-2xl bg-white"
|
||||
className="mx-4 w-full max-w-sm rounded-2xl bg-white dark:bg-[#171717]"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
>
|
||||
{selectedEventType ? (
|
||||
<>
|
||||
<View className="border-b border-gray-200 p-6">
|
||||
<Text className="mb-2 text-lg font-semibold text-gray-900">
|
||||
<View className="border-b border-gray-200 p-6 dark:border-[#4D4D4D]">
|
||||
<Text className="mb-2 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{selectedEventType.title}
|
||||
</Text>
|
||||
{selectedEventType.description ? (
|
||||
<Text className="text-sm text-gray-600">
|
||||
<Text className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{normalizeMarkdown(selectedEventType.description)}
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -731,7 +825,7 @@ export default function EventTypes() {
|
||||
|
||||
<View className="p-2">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 md:p-4"
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#2C2C2E] md:p-4"
|
||||
onPress={() => {
|
||||
setShowActionModal(false);
|
||||
const eventType = selectedEventType;
|
||||
@@ -739,12 +833,12 @@ export default function EventTypes() {
|
||||
if (eventType) handleEdit(eventType);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="pencil-outline" size={20} color="#6B7280" />
|
||||
<Text className="ml-3 text-base text-gray-900">Edit</Text>
|
||||
<Ionicons name="pencil-outline" size={20} color={theme.textSecondary} />
|
||||
<Text className="ml-3 text-base text-gray-900 dark:text-white">Edit</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 md:p-4"
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#2C2C2E] md:p-4"
|
||||
onPress={() => {
|
||||
setShowActionModal(false);
|
||||
const eventType = selectedEventType;
|
||||
@@ -752,15 +846,15 @@ export default function EventTypes() {
|
||||
if (eventType) handleDuplicate(eventType);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="copy-outline" size={20} color="#6B7280" />
|
||||
<Text className="ml-3 text-base text-gray-900">Duplicate</Text>
|
||||
<Ionicons name="copy-outline" size={20} color={theme.textSecondary} />
|
||||
<Text className="ml-3 text-base text-gray-900 dark:text-white">Duplicate</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Separator before delete button */}
|
||||
<View className="my-2 h-px bg-gray-200" />
|
||||
<View className="my-2 h-px bg-gray-200 dark:bg-[#4D4D4D]" />
|
||||
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 md:p-4"
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#2C2C2E] md:p-4"
|
||||
onPress={() => {
|
||||
setShowActionModal(false);
|
||||
const eventType = selectedEventType;
|
||||
@@ -768,20 +862,24 @@ export default function EventTypes() {
|
||||
if (eventType) handleDelete(eventType);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={20} color="#800000" />
|
||||
<Text className="ml-3 text-base text-[#800000]">Delete</Text>
|
||||
<Ionicons name="trash-outline" size={20} color={theme.destructive} />
|
||||
<Text className="ml-3 text-base" style={{ color: theme.destructive }}>
|
||||
Delete
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View className="border-t border-gray-200 p-2 md:p-4">
|
||||
<View className="border-t border-gray-200 p-2 dark:border-[#4D4D4D] md:p-4">
|
||||
<TouchableOpacity
|
||||
className="w-full rounded-lg bg-gray-100 p-3"
|
||||
className="w-full rounded-lg bg-gray-100 p-3 dark:bg-[#2C2C2E]"
|
||||
onPress={() => {
|
||||
setShowActionModal(false);
|
||||
setSelectedEventType(null);
|
||||
}}
|
||||
>
|
||||
<Text className="text-center text-base font-medium text-gray-700">Cancel</Text>
|
||||
<Text className="text-center text-base font-medium text-gray-700 dark:text-gray-300">
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
@@ -802,14 +900,16 @@ export default function EventTypes() {
|
||||
onPress={() => setShowNewModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="mx-4 w-full max-w-sm rounded-2xl bg-white"
|
||||
className="mx-4 w-full max-w-sm rounded-2xl bg-white dark:bg-[#171717]"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="border-b border-gray-200 p-6">
|
||||
<Text className="text-xl font-semibold text-gray-900">New</Text>
|
||||
<Text className="mt-1 text-sm text-gray-500">Choose what to create</Text>
|
||||
<View className="border-b border-gray-200 p-6 dark:border-[#4D4D4D]">
|
||||
<Text className="text-xl font-semibold text-gray-900 dark:text-white">New</Text>
|
||||
<Text className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
Choose what to create
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Options List */}
|
||||
@@ -823,20 +923,22 @@ export default function EventTypes() {
|
||||
handleOpenCreateModal();
|
||||
}, 100);
|
||||
}}
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 md:p-4"
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#2C2C2E] md:p-4"
|
||||
>
|
||||
<Ionicons name="calendar-outline" size={20} color="#6B7280" />
|
||||
<Text className="ml-3 text-base text-gray-900">New Event Type</Text>
|
||||
<Ionicons name="calendar-outline" size={20} color={theme.textSecondary} />
|
||||
<Text className="ml-3 text-base text-gray-900 dark:text-white">New Event Type</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Cancel button */}
|
||||
<View className="border-t border-gray-200 p-2 md:p-4">
|
||||
<View className="border-t border-gray-200 p-2 dark:border-[#4D4D4D] md:p-4">
|
||||
<TouchableOpacity
|
||||
className="w-full rounded-lg bg-gray-100 p-3"
|
||||
className="w-full rounded-lg bg-gray-100 p-3 dark:bg-[#2C2C2E]"
|
||||
onPress={() => setShowNewModal(false)}
|
||||
>
|
||||
<Text className="text-center text-base font-medium text-gray-700">Cancel</Text>
|
||||
<Text className="text-center text-base font-medium text-gray-700 dark:text-gray-300">
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -855,21 +957,21 @@ export default function EventTypes() {
|
||||
}}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-black/50 p-4">
|
||||
<View className="w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
||||
<View className="w-full max-w-md rounded-2xl bg-white shadow-2xl dark:bg-[#171717]">
|
||||
{/* Header with icon and title */}
|
||||
<View className="p-6">
|
||||
<View className="flex-row">
|
||||
{/* Danger icon */}
|
||||
<View className="mr-3 self-start rounded-full bg-red-50 p-2">
|
||||
<Ionicons name="alert-circle" size={20} color="#800000" />
|
||||
<View className="mr-3 self-start rounded-full bg-red-50 p-2 dark:bg-red-900/30">
|
||||
<Ionicons name="alert-circle" size={20} color={theme.destructive} />
|
||||
</View>
|
||||
|
||||
{/* Title and description */}
|
||||
<View className="flex-1">
|
||||
<Text className="mb-2 text-xl font-semibold text-gray-900">
|
||||
<Text className="mb-2 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Delete Event Type
|
||||
</Text>
|
||||
<Text className="text-sm leading-5 text-gray-600">
|
||||
<Text className="text-sm leading-5 text-gray-600 dark:text-gray-400">
|
||||
{eventTypeToDelete ? (
|
||||
<>
|
||||
This will permanently delete the "{eventTypeToDelete.title}" event type.
|
||||
@@ -884,22 +986,26 @@ export default function EventTypes() {
|
||||
{/* Footer with buttons */}
|
||||
<View className="flex-row-reverse gap-2 px-6 pb-6 pt-2">
|
||||
<TouchableOpacity
|
||||
className={`rounded-lg bg-gray-900 px-4 py-2.5 ${isDeleting ? "opacity-50" : ""}`}
|
||||
className={`rounded-lg bg-gray-900 px-4 py-2.5 dark:bg-white ${isDeleting ? "opacity-50" : ""}`}
|
||||
onPress={confirmDelete}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Text className="text-center text-base font-medium text-white">Delete</Text>
|
||||
<Text className="text-center text-base font-medium text-white dark:text-black">
|
||||
Delete
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2.5"
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2.5 dark:border-[#4D4D4D] dark:bg-[#2C2C2E]"
|
||||
onPress={() => {
|
||||
setShowDeleteModal(false);
|
||||
setEventTypeToDelete(null);
|
||||
}}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<Text className="text-center text-base font-medium text-gray-700">Cancel</Text>
|
||||
<Text className="text-center text-base font-medium text-gray-700 dark:text-gray-300">
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -3,7 +3,15 @@ import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Image } from "expo-image";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert, Pressable, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
Pressable,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { LogoutConfirmModal } from "@/components/LogoutConfirmModal";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useQueryContext } from "@/contexts/QueryContext";
|
||||
@@ -11,6 +19,7 @@ import { useUserProfile } from "@/hooks";
|
||||
import { showErrorAlert, showNotAvailableAlert } from "@/utils/alerts";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { getAvatarUrl } from "@/utils/getAvatarUrl";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface MoreMenuItem {
|
||||
name: string;
|
||||
@@ -26,6 +35,18 @@ export default function More() {
|
||||
const { clearCache } = useQueryContext();
|
||||
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||
const { data: userProfile } = useUserProfile();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#FFFFFF",
|
||||
backgroundSecondary: isDark ? "#171717" : "#FFFFFF",
|
||||
backgroundActive: isDark ? "#2C2C2E" : "#F8F9FA",
|
||||
text: isDark ? "#FFFFFF" : "#333333",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#C7C7CC",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
};
|
||||
|
||||
const performLogout = async () => {
|
||||
try {
|
||||
@@ -94,7 +115,7 @@ export default function More() {
|
||||
{/* iOS Native Header with Glass UI */}
|
||||
<Stack.Header
|
||||
style={{ backgroundColor: "transparent", shadowColor: "transparent" }}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : "light"}
|
||||
blurEffect={isLiquidGlassAvailable() ? undefined : isDark ? "dark" : "light"}
|
||||
>
|
||||
<Stack.Header.Title large>More</Stack.Header.Title>
|
||||
<Stack.Header.Right>
|
||||
@@ -117,64 +138,124 @@ export default function More() {
|
||||
|
||||
{/* Content */}
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: colors.background }}
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 120 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
>
|
||||
<View className="overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
style={{
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{menuItems.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={item.name}
|
||||
onPress={item.onPress}
|
||||
className={`flex-row items-center justify-between bg-white px-5 py-5 active:bg-[#F8F9FA] ${
|
||||
index < menuItems.length - 1 ? "border-b border-[#E5E5EA]" : ""
|
||||
}`}
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 20,
|
||||
borderBottomWidth: index < menuItems.length - 1 ? 1 : 0,
|
||||
borderBottomColor: colors.border,
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View className="flex-1 flex-row items-center">
|
||||
<Ionicons name={item.icon} size={20} color="#333" />
|
||||
<Text className="ml-3 text-base font-semibold text-[#333]">{item.name}</Text>
|
||||
<Ionicons name={item.icon} size={20} color={colors.text} />
|
||||
<Text style={{ color: colors.text }} className="ml-3 text-base font-semibold">
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
{item.isExternal ? (
|
||||
<Ionicons name="open-outline" size={20} color="#C7C7CC" />
|
||||
<Ionicons name="open-outline" size={20} color={colors.textSecondary} />
|
||||
) : (
|
||||
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={20} color={colors.textSecondary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Delete Account Link */}
|
||||
<View className="mt-6 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
style={{
|
||||
marginTop: 24,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Delete Account")
|
||||
}
|
||||
className="flex-row items-center justify-between bg-white px-5 py-4 active:bg-red-50"
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View className="flex-1 flex-row items-center">
|
||||
<Ionicons name="trash-outline" size={20} color="#991B1B" />
|
||||
<Text className="ml-3 text-base font-medium text-[#991B1B]">Delete Account</Text>
|
||||
<Ionicons name="trash-outline" size={20} color={theme.destructive} />
|
||||
<Text className="ml-3 text-base font-medium" style={{ color: theme.destructive }}>
|
||||
Delete Account
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="open-outline" size={20} color="#C7C7CC" />
|
||||
<Ionicons name="open-outline" size={20} color={colors.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Sign Out Button */}
|
||||
<View className="mt-4 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
style={{
|
||||
marginTop: 16,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={handleSignOut}
|
||||
className="flex-row items-center justify-center bg-white px-5 py-4 active:bg-red-50"
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 16,
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Ionicons name="log-out-outline" size={20} color="#800000" />
|
||||
<Text className="ml-2 text-base font-medium text-[#800000]">Sign Out</Text>
|
||||
<Ionicons name="log-out-outline" size={20} color={theme.destructive} />
|
||||
<Text className="ml-2 text-base font-medium" style={{ color: theme.destructive }}>
|
||||
Sign Out
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Footer Note */}
|
||||
<Text className="mt-6 px-1 text-center text-xs text-gray-400">
|
||||
<Text
|
||||
style={{ color: isDark ? "#6B7280" : "#9CA3AF" }}
|
||||
className="mt-6 px-1 text-center text-xs"
|
||||
>
|
||||
The companion app is an extension of the web application.{"\n"}
|
||||
For advanced features, visit <Text className="text-gray-800">app.cal.com</Text>
|
||||
For advanced features, visit{" "}
|
||||
<Text style={{ color: isDark ? "#D1D5DB" : "#1F2937" }}>app.cal.com</Text>
|
||||
</Text>
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useState } from "react";
|
||||
import { Alert, Platform, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Header } from "@/components/Header";
|
||||
import { LogoutConfirmModal } from "@/components/LogoutConfirmModal";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useQueryContext } from "@/contexts/QueryContext";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface MoreMenuItem {
|
||||
name: string;
|
||||
@@ -20,6 +29,9 @@ export default function More() {
|
||||
const { logout } = useAuth();
|
||||
const { clearCache } = useQueryContext();
|
||||
const [showLogoutModal, setShowLogoutModal] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const performLogout = async () => {
|
||||
try {
|
||||
@@ -88,67 +100,90 @@ export default function More() {
|
||||
];
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-1" style={{ backgroundColor: theme.background }}>
|
||||
<Header />
|
||||
<ScrollView
|
||||
contentContainerStyle={{ padding: 16, paddingBottom: 90 }}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View className="overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
className="overflow-hidden rounded-lg border border-[#E5E5EA] bg-white"
|
||||
style={{ borderColor: theme.border, backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
{menuItems.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={item.name}
|
||||
onPress={item.onPress}
|
||||
className={`flex-row items-center justify-between bg-white px-5 py-5 active:bg-[#F8F9FA] ${
|
||||
index < menuItems.length - 1 ? "border-b border-[#E5E5EA]" : ""
|
||||
}`}
|
||||
className={`flex-row items-center justify-between bg-white px-5 py-5 active:bg-[#F8F9FA]`}
|
||||
style={{
|
||||
backgroundColor: theme.backgroundSecondary,
|
||||
borderBottomWidth: index < menuItems.length - 1 ? 1 : 0,
|
||||
borderBottomColor: theme.border,
|
||||
}}
|
||||
>
|
||||
<View className="flex-1 flex-row items-center">
|
||||
<Ionicons name={item.icon} size={20} color="#333" />
|
||||
<Text className="ml-3 text-base font-semibold text-[#333]">{item.name}</Text>
|
||||
<Ionicons name={item.icon} size={20} color={theme.text} />
|
||||
<Text className="ml-3 text-base font-semibold" style={{ color: theme.text }}>
|
||||
{item.name}
|
||||
</Text>
|
||||
</View>
|
||||
{item.isExternal ? (
|
||||
<Ionicons name="open-outline" size={20} color="#C7C7CC" />
|
||||
<Ionicons name="open-outline" size={20} color={theme.textSecondary} />
|
||||
) : (
|
||||
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={20} color={theme.textSecondary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Delete Account Link */}
|
||||
<View className="mt-6 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
className="mt-6 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white"
|
||||
style={{ borderColor: theme.border, backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() =>
|
||||
openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Delete Account")
|
||||
}
|
||||
className="flex-row items-center justify-between bg-white px-5 py-4 active:bg-red-50"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<View className="flex-1 flex-row items-center">
|
||||
<Ionicons name="trash-outline" size={20} color="#991B1B" />
|
||||
<Text className="ml-3 text-base font-medium text-[#991B1B]">Delete Account</Text>
|
||||
<Ionicons name="trash-outline" size={20} color={theme.destructive} />
|
||||
<Text className="ml-3 text-base font-medium" style={{ color: theme.destructive }}>
|
||||
Delete Account
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="open-outline" size={20} color="#C7C7CC" />
|
||||
<Ionicons name="open-outline" size={20} color={theme.textSecondary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Sign Out Button */}
|
||||
<View className="mt-4 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
className="mt-4 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white"
|
||||
style={{ borderColor: theme.border, backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={handleSignOut}
|
||||
className="flex-row items-center justify-center bg-white px-5 py-4 active:bg-red-50"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<Ionicons name="log-out-outline" size={20} color="#800000" />
|
||||
<Text className="ml-2 text-base font-medium text-[#800000]">Sign Out</Text>
|
||||
<Ionicons name="log-out-outline" size={20} color={theme.destructive} />
|
||||
<Text className="ml-2 text-base font-medium" style={{ color: theme.destructive }}>
|
||||
Sign Out
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Footer Note */}
|
||||
<Text className="mt-6 px-1 text-center text-xs text-gray-400">
|
||||
<Text
|
||||
className="mt-6 px-1 text-center text-xs text-gray-400"
|
||||
style={{ color: theme.textMuted }}
|
||||
>
|
||||
The companion app is an extension of the web application.{"\n"}
|
||||
For advanced features, visit{" "}
|
||||
<Text
|
||||
className="text-gray-800"
|
||||
style={{ color: theme.text }}
|
||||
onPress={() => openInAppBrowser("https://app.cal.com", "Cal.com")}
|
||||
>
|
||||
app.cal.com
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import type { ColorValue, ImageSourcePropType } from "react-native";
|
||||
import { Tabs, VectorIcon } from "expo-router";
|
||||
import { NativeTabs } from "expo-router/unstable-native-tabs";
|
||||
import { useColorScheme } from "react-native";
|
||||
import { Platform } from "react-native";
|
||||
|
||||
// Type for vector icon families that support getImageSource
|
||||
@@ -9,22 +10,31 @@ type VectorIconFamily = {
|
||||
getImageSource: (name: string, size: number, color: ColorValue) => Promise<ImageSourcePropType>;
|
||||
};
|
||||
|
||||
const SELECTED_COLOR = "#000000";
|
||||
|
||||
export default function TabLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
selected: isDark ? "#FFFFFF" : "#000000",
|
||||
inactive: "#A3A3A3",
|
||||
background: isDark ? "#000000" : "#FFFFFF",
|
||||
border: isDark ? "#4D4D4D" : "#C6C6C8",
|
||||
indicator: isDark ? "#FFFFFF15" : "#00000015",
|
||||
};
|
||||
|
||||
if (Platform.OS === "web") {
|
||||
return <WebTabs />;
|
||||
return <WebTabs colors={colors} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<NativeTabs
|
||||
tintColor={SELECTED_COLOR}
|
||||
iconColor={Platform.select({ android: "#8E8E93", ios: undefined })}
|
||||
indicatorColor={Platform.select({ android: "#00000015", ios: undefined })}
|
||||
backgroundColor={Platform.select({ android: "#FFFFFFFF", ios: undefined })}
|
||||
tintColor={colors.selected}
|
||||
iconColor={Platform.select({ android: colors.inactive, ios: undefined })}
|
||||
indicatorColor={Platform.select({ android: colors.indicator, ios: undefined })}
|
||||
backgroundColor={Platform.select({ android: colors.background, ios: undefined })}
|
||||
labelStyle={{
|
||||
default: { color: "#8E8E93", fontSize: Platform.select({ android: 11, ios: 8.5 }) },
|
||||
selected: { color: SELECTED_COLOR, fontSize: Platform.select({ android: 12, ios: 10 }) },
|
||||
default: { color: colors.inactive, fontSize: Platform.select({ android: 11, ios: 8.5 }) },
|
||||
selected: { color: colors.selected, fontSize: Platform.select({ android: 12, ios: 10 }) },
|
||||
}}
|
||||
disableTransparentOnScrollEdge={true}
|
||||
>
|
||||
@@ -34,7 +44,7 @@ export default function TabLayout() {
|
||||
android: (
|
||||
<NativeTabs.Trigger.Icon
|
||||
src={<VectorIcon family={Ionicons as VectorIconFamily} name="link-outline" />}
|
||||
selectedColor={SELECTED_COLOR}
|
||||
selectedColor={colors.selected}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
@@ -47,7 +57,7 @@ export default function TabLayout() {
|
||||
android: (
|
||||
<NativeTabs.Trigger.Icon
|
||||
src={<VectorIcon family={Ionicons as VectorIconFamily} name="calendar-outline" />}
|
||||
selectedColor={SELECTED_COLOR}
|
||||
selectedColor={colors.selected}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
@@ -60,7 +70,7 @@ export default function TabLayout() {
|
||||
android: (
|
||||
<NativeTabs.Trigger.Icon
|
||||
src={<VectorIcon family={Ionicons as VectorIconFamily} name="time-outline" />}
|
||||
selectedColor={SELECTED_COLOR}
|
||||
selectedColor={colors.selected}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
@@ -73,7 +83,7 @@ export default function TabLayout() {
|
||||
android: (
|
||||
<NativeTabs.Trigger.Icon
|
||||
src={<VectorIcon family={Ionicons as VectorIconFamily} name="ellipsis-horizontal" />}
|
||||
selectedColor={SELECTED_COLOR}
|
||||
selectedColor={colors.selected}
|
||||
/>
|
||||
),
|
||||
})}
|
||||
@@ -83,18 +93,26 @@ export default function TabLayout() {
|
||||
);
|
||||
}
|
||||
|
||||
type TabColors = {
|
||||
selected: string;
|
||||
inactive: string;
|
||||
background: string;
|
||||
border: string;
|
||||
indicator: string;
|
||||
};
|
||||
|
||||
// TODO: Remove this once native tabs are supported on web
|
||||
function WebTabs() {
|
||||
function WebTabs({ colors }: { colors: TabColors }) {
|
||||
return (
|
||||
<Tabs
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
tabBarActiveTintColor: "#000000",
|
||||
tabBarInactiveTintColor: "#8E8E93",
|
||||
tabBarActiveTintColor: colors.selected,
|
||||
tabBarInactiveTintColor: colors.inactive,
|
||||
tabBarStyle: {
|
||||
backgroundColor: "#ffffff",
|
||||
backgroundColor: colors.background,
|
||||
borderTopWidth: 0.5,
|
||||
borderTopColor: "#C6C6C8",
|
||||
borderTopColor: colors.border,
|
||||
paddingBottom: 4,
|
||||
paddingTop: 4,
|
||||
height: Platform.OS === "android" ? 74 : 66,
|
||||
|
||||
+143
-49
@@ -1,18 +1,25 @@
|
||||
import "./_webRuntimeGuards";
|
||||
import "../global.css";
|
||||
|
||||
import { DarkTheme, DefaultTheme, ThemeProvider } from "@react-navigation/native";
|
||||
import { PortalHost } from "@rn-primitives/portal";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack } from "expo-router";
|
||||
import { Platform, StatusBar, View } from "react-native";
|
||||
import { Platform, StatusBar, useColorScheme, View } from "react-native";
|
||||
import { CalComLogo } from "@/components/CalComLogo";
|
||||
import LoginScreenComponent from "@/components/LoginScreen";
|
||||
import { NetworkStatusBanner } from "@/components/NetworkStatusBanner";
|
||||
import { GlobalToast } from "@/components/ui/GlobalToast";
|
||||
import { getColors, colors as semanticColors } from "@/constants/colors";
|
||||
import { AuthProvider, useAuth } from "@/contexts/AuthContext";
|
||||
import { QueryProvider } from "@/contexts/QueryContext";
|
||||
import { ToastProvider } from "@/contexts/ToastContext";
|
||||
import "../global.css";
|
||||
|
||||
function RootLayoutContent() {
|
||||
const { isAuthenticated, loading } = useAuth();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const colors = getColors(isDark);
|
||||
|
||||
// Show Cal.com logo while checking auth state to prevent login flash
|
||||
if (loading) {
|
||||
@@ -20,13 +27,20 @@ function RootLayoutContent() {
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: "#FFFFFF",
|
||||
backgroundColor: colors.background,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
|
||||
<CalComLogo width={120} height={26} />
|
||||
<StatusBar
|
||||
barStyle={isDark ? "light-content" : "dark-content"}
|
||||
backgroundColor={colors.background}
|
||||
/>
|
||||
<CalComLogo
|
||||
width={120}
|
||||
height={26}
|
||||
color={isDark ? semanticColors.dark.text : semanticColors.light.text}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -53,12 +67,17 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#FFFFFF",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : colors.background,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#FFFFFF",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.background,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -80,12 +99,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -106,12 +132,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -132,12 +165,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -158,12 +198,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -184,12 +231,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -210,12 +264,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -236,12 +297,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -262,12 +330,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -288,12 +363,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
@@ -314,12 +396,19 @@ function RootLayoutContent() {
|
||||
sheetInitialDetentIndex: Platform.OS === "ios" ? 0 : undefined,
|
||||
contentStyle: {
|
||||
backgroundColor:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable() ? "transparent" : "#F2F2F7",
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? "transparent"
|
||||
: colors.backgroundSecondary,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: Platform.OS === "ios" ? "transparent" : colors.backgroundSecondary,
|
||||
},
|
||||
headerBlurEffect: Platform.OS === "ios" && isLiquidGlassAvailable() ? undefined : "light",
|
||||
headerBlurEffect:
|
||||
Platform.OS === "ios" && isLiquidGlassAvailable()
|
||||
? undefined
|
||||
: isDark
|
||||
? "dark"
|
||||
: "light",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
@@ -329,8 +418,8 @@ function RootLayoutContent() {
|
||||
|
||||
const containerClass =
|
||||
Platform.OS === "web"
|
||||
? "w-[400px] flex-1 flex-col self-end border-l border-gray-200 bg-white"
|
||||
: "bg-white";
|
||||
? "w-[400px] flex-1 flex-col self-end border-l border-gray-200 dark:border-gray-700 bg-white dark:bg-black"
|
||||
: "bg-white dark:bg-black";
|
||||
|
||||
const containerStyle =
|
||||
Platform.OS === "web"
|
||||
@@ -343,15 +432,20 @@ function RootLayoutContent() {
|
||||
: { flex: 1 };
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[containerStyle, Platform.OS === "web" && { pointerEvents: "auto" as const }]}
|
||||
className={containerClass}
|
||||
>
|
||||
<StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
|
||||
{content}
|
||||
<NetworkStatusBanner />
|
||||
<PortalHost />
|
||||
</View>
|
||||
<ThemeProvider value={isDark ? DarkTheme : DefaultTheme}>
|
||||
<View
|
||||
style={[containerStyle, Platform.OS === "web" && { pointerEvents: "auto" as const }]}
|
||||
className={containerClass}
|
||||
>
|
||||
<StatusBar
|
||||
barStyle={isDark ? "light-content" : "dark-content"}
|
||||
backgroundColor={colors.background}
|
||||
/>
|
||||
{content}
|
||||
<NetworkStatusBanner />
|
||||
<PortalHost />
|
||||
</View>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// Web-only runtime guards for Companion.
|
||||
//
|
||||
// This runs as early as possible on web to prevent a noisy (but harmless)
|
||||
// react-native-css-interop error when darkMode is set to "media".
|
||||
|
||||
declare const window: unknown;
|
||||
declare const require: undefined | ((id: string) => unknown);
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
const w = window as {
|
||||
addEventListener?: (type: string, listener: (event: unknown) => void) => void;
|
||||
};
|
||||
|
||||
const shouldSuppressCssInteropError = (err: unknown) => {
|
||||
if (!(err instanceof Error)) return false;
|
||||
if (!err.message.includes("Cannot manually set color scheme")) return false;
|
||||
|
||||
// Some environments omit stack traces; if we have one, ensure it's the expected source.
|
||||
if (typeof err.stack === "string") {
|
||||
return err.stack.includes("react-native-css-interop/dist/runtime/web/color-scheme");
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const suppressIfMatch = (event: unknown, err: unknown) => {
|
||||
if (!event || typeof event !== "object") return;
|
||||
const e = event as { preventDefault?: () => void };
|
||||
if (shouldSuppressCssInteropError(err)) e.preventDefault?.();
|
||||
};
|
||||
|
||||
w.addEventListener?.("error", (event) => {
|
||||
const e = event as { error?: unknown };
|
||||
suppressIfMatch(event, e.error);
|
||||
});
|
||||
|
||||
w.addEventListener?.("unhandledrejection", (event) => {
|
||||
const e = event as { reason?: unknown };
|
||||
suppressIfMatch(event, e.reason);
|
||||
});
|
||||
|
||||
const patchCssInterop = (mod: unknown) => {
|
||||
const colorScheme = (mod as { colorScheme?: { set?: (value: unknown) => void } }).colorScheme;
|
||||
if (!colorScheme?.set) return;
|
||||
|
||||
// Completely no-op the set call when darkMode is 'media'.
|
||||
// Manual color scheme setting is not supported in this mode,
|
||||
// so we silently ignore attempts to call it.
|
||||
colorScheme.set = () => {
|
||||
// No-op: system preference controls the color scheme
|
||||
};
|
||||
};
|
||||
|
||||
// Patch react-native-css-interop on web to ignore the known error.
|
||||
// This can happen very early during init when darkMode is "media", so prefer sync require.
|
||||
try {
|
||||
if (typeof require === "function") {
|
||||
patchCssInterop(require("react-native-css-interop/dist/runtime/web/color-scheme"));
|
||||
} else {
|
||||
void import("react-native-css-interop/dist/runtime/web/color-scheme")
|
||||
.then(patchCssInterop)
|
||||
.catch(() => {
|
||||
// noop
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,15 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen";
|
||||
import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
// Semi-transparent background to prevent black flash while preserving glass effect
|
||||
const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
function getPresentationStyle(): "formSheet" | "modal" {
|
||||
if (isLiquidGlassAvailable() && osName !== "iPadOS") {
|
||||
@@ -27,6 +27,9 @@ export default function AddGuestsIOS() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [guestCount, setGuestCount] = useState(0);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const addGuestsScreenRef = useRef<AddGuestsScreenHandle>(null);
|
||||
|
||||
@@ -58,6 +61,9 @@ export default function AddGuestsIOS() {
|
||||
const presentationStyle = getPresentationStyle();
|
||||
const useGlassEffect = isLiquidGlassAvailable();
|
||||
|
||||
// Semi-transparent background to prevent flashes while preserving glass effect
|
||||
const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
const showSaveButton = guestCount > 0;
|
||||
|
||||
return (
|
||||
@@ -70,14 +76,22 @@ export default function AddGuestsIOS() {
|
||||
sheetAllowedDetents: [0.7, 1],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect
|
||||
? glassBackground
|
||||
: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.background,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header>
|
||||
<Stack.Header.Left>
|
||||
<Stack.Header.Button onPress={() => router.back()}>
|
||||
<Stack.Header.Button
|
||||
onPress={() => router.back()}
|
||||
tintColor={theme.backgroundEmphasis}
|
||||
variant="prominent"
|
||||
>
|
||||
<Stack.Header.Icon sf="xmark" />
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Left>
|
||||
@@ -90,7 +104,7 @@ export default function AddGuestsIOS() {
|
||||
onPress={handleSave}
|
||||
disabled={isSaving}
|
||||
variant="prominent"
|
||||
tintColor="#000"
|
||||
tintColor={theme.text}
|
||||
>
|
||||
<Stack.Header.Icon sf="checkmark" />
|
||||
</Stack.Header.Button>
|
||||
@@ -101,14 +115,18 @@ export default function AddGuestsIOS() {
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect
|
||||
? glassBackground
|
||||
: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.background,
|
||||
paddingTop: 56,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
) : (
|
||||
<AddGuestsScreenComponent
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { ActivityIndicator, Platform, useColorScheme, View } from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen";
|
||||
import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
@@ -15,6 +16,9 @@ export default function AddGuests() {
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const addGuestsScreenRef = useRef<AddGuestsScreenHandle>(null);
|
||||
|
||||
@@ -46,12 +50,20 @@ export default function AddGuests() {
|
||||
const renderHeaderLeft = useCallback(
|
||||
() => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable onPress={() => router.back()} className="px-2 py-2">
|
||||
<Ionicons name="close" size={24} color="#007AFF" />
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[router]
|
||||
[router, theme.border, theme.background, theme.text]
|
||||
);
|
||||
|
||||
const renderHeaderRight = useCallback(
|
||||
@@ -60,13 +72,19 @@ export default function AddGuests() {
|
||||
<AppPressable
|
||||
onPress={handleSave}
|
||||
disabled={isSaving}
|
||||
className={`px-2 py-2 ${isSaving ? "opacity-50" : ""}`}
|
||||
className={`h-10 w-10 items-center justify-center rounded-full border ${
|
||||
isSaving ? "opacity-50" : ""
|
||||
}`}
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark" size={24} color="#007AFF" />
|
||||
<Ionicons name="checkmark" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[handleSave, isSaving]
|
||||
[handleSave, isSaving, theme.text, theme.border, theme.background]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -85,8 +103,11 @@ export default function AddGuests() {
|
||||
</Stack.Header>
|
||||
)}
|
||||
|
||||
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,9 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen.ios";
|
||||
import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen.ios";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
@@ -12,7 +13,6 @@ import { showErrorAlert } from "@/utils/alerts";
|
||||
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
// Semi-transparent background to prevent black flash while preserving glass effect
|
||||
const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
function getPresentationStyle(): "formSheet" | "modal" {
|
||||
if (isLiquidGlassAvailable() && osName !== "iPadOS") {
|
||||
@@ -25,6 +25,9 @@ export default function EditAvailabilityDayIOS() {
|
||||
const { id, day } = useLocalSearchParams<{ id: string; day: string }>();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const [schedule, setSchedule] = useState<Schedule | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -62,6 +65,9 @@ export default function EditAvailabilityDayIOS() {
|
||||
const presentationStyle = getPresentationStyle();
|
||||
const useGlassEffect = isLiquidGlassAvailable();
|
||||
|
||||
// Semi-transparent background to prevent flashes while preserving glass effect
|
||||
const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -72,14 +78,22 @@ export default function EditAvailabilityDayIOS() {
|
||||
sheetAllowedDetents: [0.6, 0.9],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect
|
||||
? glassBackground
|
||||
: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.background,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header>
|
||||
<Stack.Header.Left>
|
||||
<Stack.Header.Button onPress={() => router.back()}>
|
||||
<Stack.Header.Button
|
||||
onPress={() => router.back()}
|
||||
tintColor={theme.backgroundEmphasis}
|
||||
variant="prominent"
|
||||
>
|
||||
<Stack.Header.Icon sf="xmark" />
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Left>
|
||||
@@ -91,7 +105,7 @@ export default function EditAvailabilityDayIOS() {
|
||||
onPress={handleSave}
|
||||
disabled={isSaving}
|
||||
variant="prominent"
|
||||
tintColor="#000"
|
||||
tintColor={theme.text}
|
||||
>
|
||||
<Stack.Header.Icon sf="checkmark" />
|
||||
</Stack.Header.Button>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Text, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import type { EditAvailabilityDayScreenHandle } from "@/components/screens/EditAvailabilityDayScreen";
|
||||
import EditAvailabilityDayScreenComponent from "@/components/screens/EditAvailabilityDayScreen";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
@@ -17,6 +20,9 @@ export default function EditAvailabilityDay() {
|
||||
const [schedule, setSchedule] = useState<Schedule | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const screenRef = useRef<EditAvailabilityDayScreenHandle>(null);
|
||||
|
||||
@@ -49,33 +55,79 @@ export default function EditAvailabilityDay() {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
router.back();
|
||||
}, [router]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-white"
|
||||
style={{ paddingBottom: insets.bottom }}
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ paddingBottom: insets.bottom, backgroundColor: theme.background }}
|
||||
>
|
||||
<Stack.Screen options={{ title: dayName }} />
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: dayName,
|
||||
headerStyle: {
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.text,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white" style={{ paddingBottom: insets.bottom }}>
|
||||
<View
|
||||
className="flex-1"
|
||||
style={{ paddingBottom: insets.bottom, backgroundColor: theme.background }}
|
||||
>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: dayName,
|
||||
contentStyle: {
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.text,
|
||||
},
|
||||
headerLeft: () => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable
|
||||
onPress={handleClose}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<Text
|
||||
<AppPressable
|
||||
onPress={handleSave}
|
||||
className={`text-[17px] font-semibold ${
|
||||
isSaving ? "text-gray-400" : "text-[#007AFF]"
|
||||
disabled={isSaving}
|
||||
className={`h-10 w-10 items-center justify-center rounded-full border ${
|
||||
isSaving ? "opacity-50" : ""
|
||||
}`}
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Text>
|
||||
<Ionicons name="checkmark" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen.ios";
|
||||
import { useScheduleById } from "@/hooks/useSchedules";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
// Semi-transparent background to prevent black flash while preserving glass effect
|
||||
const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
function getPresentationStyle(): "formSheet" | "modal" {
|
||||
if (isLiquidGlassAvailable() && osName !== "iPadOS") {
|
||||
@@ -22,6 +22,9 @@ export default function EditAvailabilityHoursIOS() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
// Use React Query hook to read from cache (syncs with mutations)
|
||||
const { data: schedule, isLoading, isError } = useScheduleById(id ? Number(id) : undefined);
|
||||
@@ -47,6 +50,9 @@ export default function EditAvailabilityHoursIOS() {
|
||||
const presentationStyle = getPresentationStyle();
|
||||
const useGlassEffect = isLiquidGlassAvailable();
|
||||
|
||||
// Semi-transparent background to prevent flashes while preserving glass effect
|
||||
const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -57,14 +63,22 @@ export default function EditAvailabilityHoursIOS() {
|
||||
sheetAllowedDetents: [0.7, 1],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect
|
||||
? glassBackground
|
||||
: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.background,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header>
|
||||
<Stack.Header.Left>
|
||||
<Stack.Header.Button onPress={() => router.back()}>
|
||||
<Stack.Header.Button
|
||||
onPress={() => router.back()}
|
||||
tintColor={theme.backgroundEmphasis}
|
||||
variant="prominent"
|
||||
>
|
||||
<Stack.Header.Icon sf="xmark" />
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Left>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import EditAvailabilityHoursScreenComponent from "@/components/screens/EditAvailabilityHoursScreen";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { useScheduleById } from "@/hooks/useSchedules";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
@@ -12,6 +14,9 @@ export default function EditAvailabilityHours() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
// Use React Query hook to read from cache (syncs with mutations)
|
||||
const { data: schedule, isLoading, isError } = useScheduleById(id ? Number(id) : undefined);
|
||||
@@ -41,37 +46,57 @@ export default function EditAvailabilityHours() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-white"
|
||||
style={{ paddingBottom: insets.bottom }}
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ paddingBottom: insets.bottom, backgroundColor: theme.background }}
|
||||
>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Working Hours",
|
||||
presentation: "modal",
|
||||
headerStyle: {
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.text,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white" style={{ paddingBottom: insets.bottom }}>
|
||||
<View
|
||||
className="flex-1"
|
||||
style={{ paddingBottom: insets.bottom, backgroundColor: theme.background }}
|
||||
>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Working Hours",
|
||||
presentation: "modal",
|
||||
contentStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.text,
|
||||
},
|
||||
headerLeft: () => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
|
||||
<Ionicons name="close" size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
<AppPressable
|
||||
onPress={handleClose}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen.ios";
|
||||
import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen.ios";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
// Semi-transparent background to prevent black flash while preserving glass effect
|
||||
const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
function getPresentationStyle(): "formSheet" | "modal" {
|
||||
if (isLiquidGlassAvailable() && osName !== "iPadOS") {
|
||||
@@ -23,6 +23,9 @@ export default function EditAvailabilityNameIOS() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const [schedule, setSchedule] = useState<Schedule | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -57,6 +60,9 @@ export default function EditAvailabilityNameIOS() {
|
||||
const presentationStyle = getPresentationStyle();
|
||||
const useGlassEffect = isLiquidGlassAvailable();
|
||||
|
||||
// Semi-transparent background to prevent flashes while preserving glass effect
|
||||
const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -67,14 +73,18 @@ export default function EditAvailabilityNameIOS() {
|
||||
sheetAllowedDetents: [0.7, 1],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect ? glassBackground : theme.backgroundSecondary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header>
|
||||
<Stack.Header.Left>
|
||||
<Stack.Header.Button onPress={() => router.back()}>
|
||||
<Stack.Header.Button
|
||||
onPress={() => router.back()}
|
||||
tintColor={theme.backgroundEmphasis}
|
||||
variant="prominent"
|
||||
>
|
||||
<Stack.Header.Icon sf="xmark" />
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Left>
|
||||
@@ -86,7 +96,7 @@ export default function EditAvailabilityNameIOS() {
|
||||
onPress={handleSave}
|
||||
disabled={isSaving}
|
||||
variant="prominent"
|
||||
tintColor="#000"
|
||||
tintColor={theme.text}
|
||||
>
|
||||
<Stack.Header.Icon sf="checkmark" />
|
||||
</Stack.Header.Button>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import type { EditAvailabilityNameScreenHandle } from "@/components/screens/EditAvailabilityNameScreen";
|
||||
import EditAvailabilityNameScreenComponent from "@/components/screens/EditAvailabilityNameScreen";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
@@ -16,6 +18,9 @@ export default function EditAvailabilityName() {
|
||||
const [schedule, setSchedule] = useState<Schedule | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const screenRef = useRef<EditAvailabilityNameScreenHandle>(null);
|
||||
|
||||
@@ -52,49 +57,74 @@ export default function EditAvailabilityName() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-white"
|
||||
style={{ paddingBottom: insets.bottom }}
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ paddingBottom: insets.bottom, backgroundColor: theme.background }}
|
||||
>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Edit Name & Timezone",
|
||||
presentation: "modal",
|
||||
headerStyle: {
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.text,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white" style={{ paddingBottom: insets.bottom }}>
|
||||
<View
|
||||
className="flex-1"
|
||||
style={{ paddingBottom: insets.bottom, backgroundColor: theme.background }}
|
||||
>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Edit Name & Timezone",
|
||||
presentation: "modal",
|
||||
contentStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.text,
|
||||
},
|
||||
headerLeft: () => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
|
||||
<Ionicons name="close" size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
<AppPressable
|
||||
onPress={handleClose}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<Text
|
||||
<AppPressable
|
||||
onPress={handleSave}
|
||||
className={`text-[17px] font-semibold ${
|
||||
isSaving ? "text-gray-400" : "text-[#007AFF]"
|
||||
disabled={isSaving}
|
||||
className={`h-10 w-10 items-center justify-center rounded-full border ${
|
||||
isSaving ? "opacity-50" : ""
|
||||
}`}
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Text>
|
||||
<Ionicons name="checkmark" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen.ios";
|
||||
import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen.ios";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
// Semi-transparent background to prevent black flash while preserving glass effect
|
||||
const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
function getPresentationStyle(): "formSheet" | "modal" {
|
||||
if (isLiquidGlassAvailable() && osName !== "iPadOS") {
|
||||
@@ -26,6 +26,9 @@ export default function EditAvailabilityOverrideIOS() {
|
||||
}>();
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const [schedule, setSchedule] = useState<Schedule | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
@@ -72,6 +75,9 @@ export default function EditAvailabilityOverrideIOS() {
|
||||
const presentationStyle = getPresentationStyle();
|
||||
const useGlassEffect = isLiquidGlassAvailable();
|
||||
|
||||
// Semi-transparent background to prevent flashes while preserving glass effect
|
||||
const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -82,14 +88,22 @@ export default function EditAvailabilityOverrideIOS() {
|
||||
sheetAllowedDetents: [0.7, 1],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect
|
||||
? glassBackground
|
||||
: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.background,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header>
|
||||
<Stack.Header.Left>
|
||||
<Stack.Header.Button onPress={() => router.back()}>
|
||||
<Stack.Header.Button
|
||||
onPress={() => router.back()}
|
||||
tintColor={theme.backgroundEmphasis}
|
||||
variant="prominent"
|
||||
>
|
||||
<Stack.Header.Icon sf="xmark" />
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Left>
|
||||
@@ -101,7 +115,7 @@ export default function EditAvailabilityOverrideIOS() {
|
||||
onPress={handleSave}
|
||||
disabled={isSaving}
|
||||
variant="prominent"
|
||||
tintColor="#000"
|
||||
tintColor={theme.text}
|
||||
>
|
||||
<Stack.Header.Icon sf="checkmark" />
|
||||
</Stack.Header.Button>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Text, TouchableOpacity, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import type { EditAvailabilityOverrideScreenHandle } from "@/components/screens/EditAvailabilityOverrideScreen";
|
||||
import EditAvailabilityOverrideScreenComponent from "@/components/screens/EditAvailabilityOverrideScreen";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
@@ -19,6 +21,9 @@ export default function EditAvailabilityOverride() {
|
||||
const [schedule, setSchedule] = useState<Schedule | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const screenRef = useRef<EditAvailabilityOverrideScreenHandle>(null);
|
||||
|
||||
@@ -69,49 +74,74 @@ export default function EditAvailabilityOverride() {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View
|
||||
className="flex-1 items-center justify-center bg-white"
|
||||
style={{ paddingBottom: insets.bottom }}
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ paddingBottom: insets.bottom, backgroundColor: theme.background }}
|
||||
>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title,
|
||||
presentation: "modal",
|
||||
headerStyle: {
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.text,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white" style={{ paddingBottom: insets.bottom }}>
|
||||
<View
|
||||
className="flex-1"
|
||||
style={{ paddingBottom: insets.bottom, backgroundColor: theme.background }}
|
||||
>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title,
|
||||
presentation: "modal",
|
||||
contentStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.text,
|
||||
},
|
||||
headerLeft: () => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
|
||||
<Ionicons name="close" size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
<AppPressable
|
||||
onPress={handleClose}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
headerRight: () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<Text
|
||||
<AppPressable
|
||||
onPress={handleSave}
|
||||
className={`text-[17px] font-semibold ${
|
||||
isSaving ? "text-gray-400" : "text-[#007AFF]"
|
||||
disabled={isSaving}
|
||||
className={`h-10 w-10 items-center justify-center rounded-full border ${
|
||||
isSaving ? "opacity-50" : ""
|
||||
}`}
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Text>
|
||||
<Ionicons name="checkmark" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen";
|
||||
import EditLocationScreenComponent from "@/components/screens/EditLocationScreen";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
// Semi-transparent background to prevent black flash while preserving glass effect
|
||||
const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
function getPresentationStyle(): "formSheet" | "modal" {
|
||||
if (isLiquidGlassAvailable() && osName !== "iPadOS") {
|
||||
@@ -26,6 +26,9 @@ export default function EditLocationIOS() {
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const editLocationScreenRef = useRef<EditLocationScreenHandle>(null);
|
||||
|
||||
@@ -57,6 +60,9 @@ export default function EditLocationIOS() {
|
||||
const presentationStyle = getPresentationStyle();
|
||||
const useGlassEffect = isLiquidGlassAvailable();
|
||||
|
||||
// Semi-transparent background to prevent flashes while preserving glass effect
|
||||
const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -67,14 +73,18 @@ export default function EditLocationIOS() {
|
||||
sheetAllowedDetents: [0.7, 1],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect ? glassBackground : theme.backgroundSecondary,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header>
|
||||
<Stack.Header.Left>
|
||||
<Stack.Header.Button onPress={() => router.back()}>
|
||||
<Stack.Header.Button
|
||||
onPress={() => router.back()}
|
||||
tintColor={theme.backgroundEmphasis}
|
||||
variant="prominent"
|
||||
>
|
||||
<Stack.Header.Icon sf="xmark" />
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Left>
|
||||
@@ -86,7 +96,7 @@ export default function EditLocationIOS() {
|
||||
onPress={handleSave}
|
||||
disabled={isSaving}
|
||||
variant="prominent"
|
||||
tintColor="#000"
|
||||
tintColor={theme.text}
|
||||
>
|
||||
<Stack.Header.Icon sf="checkmark" />
|
||||
</Stack.Header.Button>
|
||||
@@ -96,14 +106,14 @@ export default function EditLocationIOS() {
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect ? glassBackground : theme.backgroundSecondary,
|
||||
paddingTop: 56,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
) : (
|
||||
<EditLocationScreenComponent
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { ActivityIndicator, Platform, useColorScheme, View } from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen";
|
||||
import EditLocationScreenComponent from "@/components/screens/EditLocationScreen";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
@@ -15,6 +16,9 @@ export default function EditLocation() {
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const editLocationScreenRef = useRef<EditLocationScreenHandle>(null);
|
||||
|
||||
@@ -46,12 +50,20 @@ export default function EditLocation() {
|
||||
const renderHeaderLeft = useCallback(
|
||||
() => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable onPress={() => router.back()} className="px-2 py-2">
|
||||
<Ionicons name="close" size={24} color="#007AFF" />
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[router]
|
||||
[router, theme.border, theme.background, theme.text]
|
||||
);
|
||||
|
||||
const renderHeaderRight = useCallback(
|
||||
@@ -60,13 +72,19 @@ export default function EditLocation() {
|
||||
<AppPressable
|
||||
onPress={handleSave}
|
||||
disabled={isSaving}
|
||||
className={`px-2 py-2 ${isSaving ? "opacity-50" : ""}`}
|
||||
className={`h-10 w-10 items-center justify-center rounded-full border ${
|
||||
isSaving ? "opacity-50" : ""
|
||||
}`}
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark" size={24} color="#007AFF" />
|
||||
<Ionicons name="checkmark" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[handleSave, isSaving]
|
||||
[handleSave, isSaving, theme.text, theme.border, theme.background]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -85,8 +103,11 @@ export default function EditLocation() {
|
||||
</Stack.Header>
|
||||
)}
|
||||
|
||||
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
@@ -98,6 +119,15 @@ export default function EditLocation() {
|
||||
options={{
|
||||
title: "Edit Location",
|
||||
headerBackButtonDisplayMode: "minimal",
|
||||
contentStyle: {
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: theme.background,
|
||||
},
|
||||
headerTitleStyle: {
|
||||
color: theme.text,
|
||||
},
|
||||
headerLeft: Platform.OS !== "ios" ? renderHeaderLeft : undefined,
|
||||
headerRight: Platform.OS !== "ios" ? renderHeaderRight : undefined,
|
||||
}}
|
||||
|
||||
@@ -2,8 +2,9 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
@@ -37,6 +38,9 @@ export default function MarkNoShowIOS() {
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [attendees, setAttendees] = useState<Attendee[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
useEffect(() => {
|
||||
if (uid) {
|
||||
@@ -82,7 +86,7 @@ export default function MarkNoShowIOS() {
|
||||
sheetAllowedDetents: [0.7, 1],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect ? "transparent" : theme.background,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -100,14 +104,14 @@ export default function MarkNoShowIOS() {
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: useGlassEffect ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect ? "transparent" : theme.background,
|
||||
paddingTop: 56,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
) : (
|
||||
<MarkNoShowScreenComponent
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { ActivityIndicator, Platform, useColorScheme, View } from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
@@ -29,6 +30,9 @@ export default function MarkNoShow() {
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [attendees, setAttendees] = useState<Attendee[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
useEffect(() => {
|
||||
if (uid) {
|
||||
@@ -65,12 +69,20 @@ export default function MarkNoShow() {
|
||||
const renderHeaderLeft = useCallback(
|
||||
() => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable onPress={() => router.back()} className="px-2 py-2">
|
||||
<Ionicons name="close" size={24} color="#007AFF" />
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[router]
|
||||
[router, theme.border, theme.background, theme.text]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -89,8 +101,11 @@ export default function MarkNoShow() {
|
||||
</Stack.Header>
|
||||
)}
|
||||
|
||||
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,9 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import type { ConferencingSession } from "@/services/types/bookings.types";
|
||||
@@ -27,6 +28,9 @@ export default function MeetingSessionDetailsIOS() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [sessions, setSessions] = useState<ConferencingSession[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
useEffect(() => {
|
||||
if (uid) {
|
||||
@@ -60,7 +64,7 @@ export default function MeetingSessionDetailsIOS() {
|
||||
sheetAllowedDetents: [0.7, 1],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect ? "transparent" : theme.background,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -78,14 +82,14 @@ export default function MeetingSessionDetailsIOS() {
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: useGlassEffect ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect ? "transparent" : theme.background,
|
||||
paddingTop: 56,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
) : (
|
||||
<MeetingSessionDetailsScreenComponent
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { ActivityIndicator, Platform, useColorScheme, View } from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import type { ConferencingSession } from "@/services/types/bookings.types";
|
||||
@@ -14,6 +15,9 @@ export default function MeetingSessionDetails() {
|
||||
const router = useRouter();
|
||||
const [sessions, setSessions] = useState<ConferencingSession[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
useEffect(() => {
|
||||
if (uid) {
|
||||
@@ -37,12 +41,20 @@ export default function MeetingSessionDetails() {
|
||||
const renderHeaderLeft = useCallback(
|
||||
() => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable onPress={() => router.back()} className="px-2 py-2">
|
||||
<Ionicons name="close" size={24} color="#007AFF" />
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[router]
|
||||
[router, theme.border, theme.background, theme.text]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -61,8 +73,11 @@ export default function MeetingSessionDetails() {
|
||||
</Stack.Header>
|
||||
)}
|
||||
|
||||
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -97,10 +97,12 @@ export default function OAuthCallback() {
|
||||
}, [params, router, auth.userInfo]);
|
||||
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-white">
|
||||
<View className="flex-1 items-center justify-center bg-white dark:bg-black">
|
||||
<ActivityIndicator size="large" color="#2563eb" />
|
||||
<Text className="mt-4 text-lg text-gray-700">Processing OAuth callback...</Text>
|
||||
<Text className="mt-2 text-sm text-gray-500">
|
||||
<Text className="mt-4 text-lg text-gray-700 dark:text-gray-300">
|
||||
Processing OAuth callback...
|
||||
</Text>
|
||||
<Text className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Please wait while we complete the authentication.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -4,8 +4,16 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { ActivityIndicator, Image, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { useUserProfile } from "@/hooks";
|
||||
import { showSuccessAlert } from "@/utils/alerts";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
@@ -35,6 +43,19 @@ export default function ProfileSheet() {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { data: userProfile, isLoading } = useUserProfile();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#111827",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
icon: isDark ? "#FFFFFF" : "#374151",
|
||||
iconSecondary: isDark ? "#636366" : "#9CA3AF",
|
||||
avatarBg: isDark ? "#4D4D4D" : "#E5E7EB",
|
||||
avatarText: isDark ? "#FFFFFF" : "#4B5563",
|
||||
};
|
||||
|
||||
const publicPageUrl = userProfile?.username ? `https://cal.com/${userProfile.username}` : null;
|
||||
|
||||
@@ -103,6 +124,9 @@ export default function ProfileSheet() {
|
||||
const presentationStyle = getPresentationStyle();
|
||||
const useGlassEffect = isLiquidGlassAvailable();
|
||||
|
||||
// Semi-transparent background to prevent flashes while preserving glass effect
|
||||
const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -116,25 +140,44 @@ export default function ProfileSheet() {
|
||||
sheetAllowedDetents: [0.7, 0.9],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? "transparent" : "#FFFFFF",
|
||||
backgroundColor: useGlassEffect
|
||||
? glassBackground
|
||||
: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.background,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
headerBlurEffect: useGlassEffect ? undefined : "light",
|
||||
headerBlurEffect: useGlassEffect ? undefined : isDark ? "dark" : "light",
|
||||
headerTintColor: colors.text,
|
||||
headerLeft: () => null,
|
||||
headerRight: () => (
|
||||
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
|
||||
<Ionicons name="close" size={24} color="#000" />
|
||||
</TouchableOpacity>
|
||||
),
|
||||
headerRight: () => null,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header>
|
||||
<Stack.Header.Title>You</Stack.Header.Title>
|
||||
|
||||
<Stack.Header.Right>
|
||||
<Stack.Header.Button
|
||||
onPress={handleClose}
|
||||
variant="prominent"
|
||||
tintColor={theme.backgroundEmphasis}
|
||||
>
|
||||
<Stack.Header.Icon sf="xmark" />
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Right>
|
||||
</Stack.Header>
|
||||
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: useGlassEffect ? "transparent" : "#FFFFFF",
|
||||
backgroundColor: useGlassEffect
|
||||
? glassBackground
|
||||
: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.background,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
@@ -142,7 +185,7 @@ export default function ProfileSheet() {
|
||||
<View className="mt-20 px-6">
|
||||
{isLoading ? (
|
||||
<View className="items-center py-8">
|
||||
<ActivityIndicator size="large" color="#000" />
|
||||
<ActivityIndicator size="large" color={colors.text} />
|
||||
</View>
|
||||
) : (
|
||||
<View className="flex-row items-center">
|
||||
@@ -154,10 +197,10 @@ export default function ProfileSheet() {
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
className="items-center justify-center rounded-full bg-gray-200"
|
||||
style={{ width: 64, height: 64 }}
|
||||
className="items-center justify-center rounded-full"
|
||||
style={{ width: 64, height: 64, backgroundColor: colors.avatarBg }}
|
||||
>
|
||||
<Text className="text-2xl font-semibold text-gray-600">
|
||||
<Text className="text-2xl font-semibold" style={{ color: colors.avatarText }}>
|
||||
{userProfile?.name?.charAt(0).toUpperCase() ||
|
||||
userProfile?.email?.charAt(0).toUpperCase() ||
|
||||
"?"}
|
||||
@@ -167,11 +210,13 @@ export default function ProfileSheet() {
|
||||
|
||||
{/* Name and Status */}
|
||||
<View className="ml-4 flex-1">
|
||||
<Text className="text-xl font-semibold text-gray-900">
|
||||
<Text className="text-xl font-semibold" style={{ color: colors.text }}>
|
||||
{userProfile?.name || "User"}
|
||||
</Text>
|
||||
{userProfile?.email ? (
|
||||
<Text className="mt-1 text-sm text-gray-500">{userProfile.email}</Text>
|
||||
<Text className="mt-1 text-sm" style={{ color: colors.textSecondary }}>
|
||||
{userProfile.email}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
@@ -183,19 +228,22 @@ export default function ProfileSheet() {
|
||||
{menuItems.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
className={`flex-row items-center justify-between rounded-xl px-4 py-4 active:bg-gray-100 ${
|
||||
className={`flex-row items-center justify-between rounded-xl px-4 py-4 ${
|
||||
index < menuItems.length - 1 ? "mb-1" : ""
|
||||
}`}
|
||||
style={{ backgroundColor: "transparent" }}
|
||||
onPress={item.onPress}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons name={item.icon} size={22} color="#374151" />
|
||||
<Text className="ml-4 text-base text-gray-900">{item.label}</Text>
|
||||
<Ionicons name={item.icon} size={22} color={colors.icon} />
|
||||
<Text className="ml-4 text-base" style={{ color: colors.text }}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</View>
|
||||
{item.external ? (
|
||||
<Ionicons name="open-outline" size={18} color="#9CA3AF" />
|
||||
<Ionicons name="open-outline" size={18} color={colors.iconSecondary} />
|
||||
) : (
|
||||
<Ionicons name="chevron-forward" size={18} color="#9CA3AF" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.iconSecondary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
||||
@@ -2,7 +2,15 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { ActivityIndicator, Image, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
ScrollView,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import { useUserProfile } from "@/hooks";
|
||||
@@ -22,6 +30,19 @@ export default function ProfileSheet() {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { data: userProfile, isLoading } = useUserProfile();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#111827",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
icon: isDark ? "#FFFFFF" : "#374151",
|
||||
iconSecondary: isDark ? "#636366" : "#9CA3AF",
|
||||
avatarBg: isDark ? "#4D4D4D" : "#E5E7EB",
|
||||
avatarText: isDark ? "#FFFFFF" : "#4B5563",
|
||||
activeBackground: isDark ? "#171717" : "#F3F4F6",
|
||||
};
|
||||
|
||||
const publicPageUrl = userProfile?.username ? `https://cal.com/${userProfile.username}` : null;
|
||||
|
||||
@@ -103,16 +124,17 @@ export default function ProfileSheet() {
|
||||
title: "Profile",
|
||||
presentation: "modal",
|
||||
contentStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
headerStyle: {
|
||||
backgroundColor: "#FFFFFF",
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
headerTintColor: colors.text,
|
||||
headerLeft: () => null,
|
||||
headerRight: () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<TouchableOpacity onPress={handleClose} style={{ padding: 8 }}>
|
||||
<Ionicons name="close" size={24} color="#000" />
|
||||
<Ionicons name="close" size={24} color={colors.text} />
|
||||
</TouchableOpacity>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
@@ -120,14 +142,14 @@ export default function ProfileSheet() {
|
||||
/>
|
||||
|
||||
<ScrollView
|
||||
style={{ flex: 1, backgroundColor: "#FFFFFF" }}
|
||||
style={{ flex: 1, backgroundColor: colors.background }}
|
||||
contentContainerStyle={{ paddingBottom: insets.bottom + 20 }}
|
||||
>
|
||||
{/* Profile Header */}
|
||||
<View className="px-6 py-6">
|
||||
{isLoading ? (
|
||||
<View className="items-center py-8">
|
||||
<ActivityIndicator size="large" color="#000" />
|
||||
<ActivityIndicator size="large" color={colors.text} />
|
||||
</View>
|
||||
) : (
|
||||
<View className="flex-row items-center">
|
||||
@@ -139,10 +161,10 @@ export default function ProfileSheet() {
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
className="items-center justify-center rounded-full bg-gray-200"
|
||||
style={{ width: 64, height: 64 }}
|
||||
className="items-center justify-center rounded-full"
|
||||
style={{ width: 64, height: 64, backgroundColor: colors.avatarBg }}
|
||||
>
|
||||
<Text className="text-2xl font-semibold text-gray-600">
|
||||
<Text className="text-2xl font-semibold" style={{ color: colors.avatarText }}>
|
||||
{userProfile?.name?.charAt(0).toUpperCase() ||
|
||||
userProfile?.email?.charAt(0).toUpperCase() ||
|
||||
"?"}
|
||||
@@ -152,11 +174,13 @@ export default function ProfileSheet() {
|
||||
|
||||
{/* Name and Email */}
|
||||
<View className="ml-4 flex-1">
|
||||
<Text className="text-xl font-semibold text-gray-900">
|
||||
<Text className="text-xl font-semibold" style={{ color: colors.text }}>
|
||||
{userProfile?.name || "User"}
|
||||
</Text>
|
||||
{userProfile?.email ? (
|
||||
<Text className="mt-1 text-sm text-gray-500">{userProfile.email}</Text>
|
||||
<Text className="mt-1 text-sm" style={{ color: colors.textSecondary }}>
|
||||
{userProfile.email}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
@@ -168,19 +192,22 @@ export default function ProfileSheet() {
|
||||
{menuItems.map((item, index) => (
|
||||
<TouchableOpacity
|
||||
key={item.id}
|
||||
className={`flex-row items-center justify-between rounded-xl px-4 py-4 active:bg-gray-100 ${
|
||||
className={`flex-row items-center justify-between rounded-xl px-4 py-4 ${
|
||||
index < menuItems.length - 1 ? "mb-1" : ""
|
||||
}`}
|
||||
style={{ backgroundColor: "transparent" }}
|
||||
onPress={item.onPress}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons name={item.icon} size={22} color="#374151" />
|
||||
<Text className="ml-4 text-base text-gray-900">{item.label}</Text>
|
||||
<Ionicons name={item.icon} size={22} color={colors.icon} />
|
||||
<Text className="ml-4 text-base" style={{ color: colors.text }}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</View>
|
||||
{item.external ? (
|
||||
<Ionicons name="open-outline" size={18} color="#9CA3AF" />
|
||||
<Ionicons name="open-outline" size={18} color={colors.iconSecondary} />
|
||||
) : (
|
||||
<Ionicons name="chevron-forward" size={18} color="#9CA3AF" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.iconSecondary} />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
|
||||
@@ -2,15 +2,15 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen";
|
||||
import RescheduleScreenComponent from "@/components/screens/RescheduleScreen";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
// Semi-transparent background to prevent black flash while preserving glass effect
|
||||
const GLASS_BACKGROUND = "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
function getPresentationStyle(): "formSheet" | "modal" {
|
||||
if (isLiquidGlassAvailable() && osName !== "iPadOS") {
|
||||
@@ -26,6 +26,9 @@ export default function RescheduleIOS() {
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const rescheduleScreenRef = useRef<RescheduleScreenHandle>(null);
|
||||
|
||||
@@ -57,6 +60,9 @@ export default function RescheduleIOS() {
|
||||
const presentationStyle = getPresentationStyle();
|
||||
const useGlassEffect = isLiquidGlassAvailable();
|
||||
|
||||
// Semi-transparent background to prevent flashes while preserving glass effect
|
||||
const glassBackground = isDark ? "rgba(28, 28, 30, 0.01)" : "rgba(248, 248, 250, 0.01)";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -67,14 +73,22 @@ export default function RescheduleIOS() {
|
||||
sheetAllowedDetents: [0.7, 1],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect
|
||||
? glassBackground
|
||||
: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.background,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Stack.Header>
|
||||
<Stack.Header.Left>
|
||||
<Stack.Header.Button onPress={() => router.back()}>
|
||||
<Stack.Header.Button
|
||||
onPress={() => router.back()}
|
||||
tintColor={theme.backgroundEmphasis}
|
||||
variant="prominent"
|
||||
>
|
||||
<Stack.Header.Icon sf="xmark" />
|
||||
</Stack.Header.Button>
|
||||
</Stack.Header.Left>
|
||||
@@ -86,7 +100,7 @@ export default function RescheduleIOS() {
|
||||
onPress={handleSave}
|
||||
disabled={isSaving}
|
||||
variant="prominent"
|
||||
tintColor="#000"
|
||||
tintColor={theme.text}
|
||||
>
|
||||
<Stack.Header.Icon sf="checkmark" />
|
||||
</Stack.Header.Button>
|
||||
@@ -96,14 +110,18 @@ export default function RescheduleIOS() {
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: useGlassEffect ? GLASS_BACKGROUND : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect
|
||||
? glassBackground
|
||||
: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.background,
|
||||
paddingTop: 56,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
) : (
|
||||
<RescheduleScreenComponent
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { ActivityIndicator, Platform, useColorScheme, View } from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen";
|
||||
import RescheduleScreenComponent from "@/components/screens/RescheduleScreen";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
@@ -15,6 +16,9 @@ export default function Reschedule() {
|
||||
const [booking, setBooking] = useState<Booking | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const rescheduleScreenRef = useRef<RescheduleScreenHandle>(null);
|
||||
|
||||
@@ -46,12 +50,20 @@ export default function Reschedule() {
|
||||
const renderHeaderLeft = useCallback(
|
||||
() => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable onPress={() => router.back()} className="px-2 py-2">
|
||||
<Ionicons name="close" size={24} color="#007AFF" />
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[router]
|
||||
[router, theme.border, theme.background, theme.text]
|
||||
);
|
||||
|
||||
const renderHeaderRight = useCallback(
|
||||
@@ -60,13 +72,19 @@ export default function Reschedule() {
|
||||
<AppPressable
|
||||
onPress={handleSave}
|
||||
disabled={isSaving}
|
||||
className={`px-2 py-2 ${isSaving ? "opacity-50" : ""}`}
|
||||
className={`h-10 w-10 items-center justify-center rounded-full border ${
|
||||
isSaving ? "opacity-50" : ""
|
||||
}`}
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark" size={24} color="#007AFF" />
|
||||
<Ionicons name="checkmark" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[handleSave, isSaving]
|
||||
[handleSave, isSaving, theme.text, theme.border, theme.background]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -85,8 +103,11 @@ export default function Reschedule() {
|
||||
</Stack.Header>
|
||||
)}
|
||||
|
||||
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,9 @@ import { osName } from "expo-device";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, View } from "react-native";
|
||||
import { ActivityIndicator, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import type { BookingRecording } from "@/services/types/bookings.types";
|
||||
@@ -23,6 +24,9 @@ export default function ViewRecordingsIOS() {
|
||||
const insets = useSafeAreaInsets();
|
||||
const [recordings, setRecordings] = useState<BookingRecording[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
useEffect(() => {
|
||||
if (uid) {
|
||||
@@ -55,7 +59,7 @@ export default function ViewRecordingsIOS() {
|
||||
sheetAllowedDetents: [0.7, 1],
|
||||
sheetInitialDetentIndex: 0,
|
||||
contentStyle: {
|
||||
backgroundColor: useGlassEffect ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect ? "transparent" : theme.background,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -73,14 +77,14 @@ export default function ViewRecordingsIOS() {
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: useGlassEffect ? "transparent" : "#F2F2F7",
|
||||
backgroundColor: useGlassEffect ? "transparent" : theme.background,
|
||||
paddingTop: 56,
|
||||
paddingBottom: insets.bottom,
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
) : (
|
||||
<ViewRecordingsScreenComponent
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { ActivityIndicator, Platform, useColorScheme, View } from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import type { BookingRecording } from "@/services/types/bookings.types";
|
||||
@@ -15,6 +16,9 @@ export default function ViewRecordings() {
|
||||
const router = useRouter();
|
||||
const [recordings, setRecordings] = useState<BookingRecording[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
useEffect(() => {
|
||||
if (uid) {
|
||||
@@ -37,12 +41,20 @@ export default function ViewRecordings() {
|
||||
const renderHeaderLeft = useCallback(
|
||||
() => (
|
||||
<HeaderButtonWrapper side="left">
|
||||
<AppPressable onPress={() => router.back()} className="px-2 py-2">
|
||||
<Ionicons name="close" size={24} color="#007AFF" />
|
||||
<AppPressable
|
||||
onPress={() => router.back()}
|
||||
className="h-10 w-10 items-center justify-center rounded-full border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={20} color={theme.text} />
|
||||
</AppPressable>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
[router]
|
||||
[router, theme.border, theme.background, theme.text]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
@@ -61,8 +73,11 @@ export default function ViewRecordings() {
|
||||
</Stack.Header>
|
||||
)}
|
||||
|
||||
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
*/
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import { ScrollView, Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import type { BookingActionsResult } from "@/utils/booking-actions";
|
||||
import { FullScreenModal } from "./FullScreenModal";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
export interface BookingActionsModalProps {
|
||||
visible: boolean;
|
||||
@@ -60,10 +61,14 @@ function ActionButton({
|
||||
isDanger = false,
|
||||
isLast = false,
|
||||
}: ActionButtonProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
const iconColor = !enabled ? "#C7C7CC" : isDanger ? "#FF3B30" : "#007AFF";
|
||||
const textColor = !enabled ? "#C7C7CC" : isDanger ? "#FF3B30" : "#000";
|
||||
const iconColor = !enabled ? "#C7C7CC" : isDanger ? theme.destructive : "#007AFF";
|
||||
const textColor = !enabled ? "#C7C7CC" : isDanger ? theme.destructive : theme.text;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -72,8 +77,8 @@ function ActionButton({
|
||||
onPress();
|
||||
}}
|
||||
disabled={!enabled}
|
||||
className={`flex-row items-center px-4 py-3.5 active:bg-[#F8F9FA] ${
|
||||
!isLast ? "border-b border-gray-100" : ""
|
||||
className={`flex-row items-center px-4 py-3.5 active:bg-[#F8F9FA] dark:active:bg-[#262626] ${
|
||||
!isLast ? "border-b border-gray-100 dark:border-[#4D4D4D]" : ""
|
||||
}`}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
@@ -87,8 +92,8 @@ function ActionButton({
|
||||
{label}
|
||||
</Text>
|
||||
{!enabled && (
|
||||
<View className="rounded bg-gray-100 px-2 py-0.5">
|
||||
<Text className="text-xs text-gray-500">Unavailable</Text>
|
||||
<View className="rounded bg-gray-100 px-2 py-0.5 dark:bg-[#262626]">
|
||||
<Text className="text-xs text-gray-500 dark:text-[#A3A3A3]">Unavailable</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@@ -101,8 +106,10 @@ interface SectionHeaderProps {
|
||||
|
||||
function SectionHeader({ title }: SectionHeaderProps) {
|
||||
return (
|
||||
<View className="bg-[#F2F2F7] px-4 py-2">
|
||||
<Text className="text-[13px] font-medium uppercase tracking-wide text-gray-500">{title}</Text>
|
||||
<View className="bg-[#F2F2F7] px-4 py-2 dark:bg-[#262626]">
|
||||
<Text className="text-[13px] font-medium uppercase tracking-wide text-gray-500 dark:text-[#A3A3A3]">
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -248,10 +255,13 @@ export function BookingActionsModal({
|
||||
style={{ paddingBottom: insets.bottom || 16 }}
|
||||
>
|
||||
{/* Actions Card */}
|
||||
<View className="mx-4 mb-3 overflow-hidden rounded-2xl bg-white">
|
||||
<View className="mx-4 mb-3 overflow-hidden rounded-2xl bg-white dark:bg-[#171717]">
|
||||
{/* Booking Title Header */}
|
||||
<View className="border-b border-gray-100 px-4 py-4">
|
||||
<Text className="text-center text-[13px] font-medium text-gray-500" numberOfLines={1}>
|
||||
<View className="border-b border-gray-100 px-4 py-4 dark:border-[#4D4D4D]">
|
||||
<Text
|
||||
className="text-center text-[13px] font-medium text-gray-500 dark:text-[#A3A3A3]"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{booking.title}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -316,7 +326,7 @@ export function BookingActionsModal({
|
||||
|
||||
{/* Cancel Button */}
|
||||
<TouchableOpacity
|
||||
className="mx-4 overflow-hidden rounded-2xl bg-white"
|
||||
className="mx-4 overflow-hidden rounded-2xl bg-white dark:bg-[#171717]"
|
||||
onPress={onClose}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
*/
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import { ScrollView, Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import type { BookingActionsResult } from "@/utils/booking-actions";
|
||||
import { FullScreenModal } from "./FullScreenModal";
|
||||
@@ -64,10 +65,13 @@ function ActionButton({
|
||||
isDanger = false,
|
||||
isLast = false,
|
||||
}: ActionButtonProps) {
|
||||
if (!visible) return null;
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const iconColor = !enabled ? "#D1D5DB" : isDanger ? "#800020" : "#6B7280";
|
||||
const textColor = !enabled ? "#D1D5DB" : isDanger ? "#800020" : "#111827";
|
||||
if (!visible) return null;
|
||||
const iconColor = !enabled ? "#D1D5DB" : isDanger ? theme.destructive : "#6B7280";
|
||||
const textColor = !enabled ? "#D1D5DB" : isDanger ? theme.destructive : "#111827";
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
@@ -76,8 +80,8 @@ function ActionButton({
|
||||
onPress();
|
||||
}}
|
||||
disabled={!enabled}
|
||||
className={`flex-row items-center px-4 py-3 active:bg-gray-50 ${
|
||||
!isLast ? "border-b border-gray-100" : ""
|
||||
className={`flex-row items-center px-4 py-3 active:bg-gray-50 dark:active:bg-[#262626] ${
|
||||
!isLast ? "border-b border-gray-100 dark:border-[#4D4D4D]" : ""
|
||||
}`}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
@@ -88,8 +92,8 @@ function ActionButton({
|
||||
{label}
|
||||
</Text>
|
||||
{!enabled && (
|
||||
<View className="rounded bg-gray-100 px-2 py-0.5">
|
||||
<Text className="text-xs text-gray-500">Unavailable</Text>
|
||||
<View className="rounded bg-gray-100 px-2 py-0.5 dark:bg-[#262626]">
|
||||
<Text className="text-xs text-gray-500 dark:text-[#A3A3A3]">Unavailable</Text>
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@@ -102,8 +106,8 @@ interface SectionHeaderProps {
|
||||
|
||||
function SectionHeader({ title }: SectionHeaderProps) {
|
||||
return (
|
||||
<View className="bg-gray-50 px-4 py-2">
|
||||
<Text className="text-[12px] font-semibold uppercase tracking-wide text-gray-500">
|
||||
<View className="bg-gray-50 px-4 py-2 dark:bg-[#262626]">
|
||||
<Text className="text-[12px] font-semibold uppercase tracking-wide text-gray-500 dark:text-[#A3A3A3]">
|
||||
{title}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -248,10 +252,13 @@ export function BookingActionsModal({
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Actions Card */}
|
||||
<View className="mb-4 overflow-hidden rounded-2xl bg-white shadow-lg">
|
||||
<View className="mb-4 overflow-hidden rounded-2xl bg-white shadow-lg dark:bg-[#171717]">
|
||||
{/* Booking Title Header */}
|
||||
<View className="border-b border-gray-100 px-4 py-3">
|
||||
<Text className="text-center text-[14px] font-medium text-gray-600" numberOfLines={1}>
|
||||
<View className="border-b border-gray-100 px-4 py-3 dark:border-[#4D4D4D]">
|
||||
<Text
|
||||
className="text-center text-[14px] font-medium text-gray-600 dark:text-[#A3A3A3]"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{booking.title}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -316,12 +323,14 @@ export function BookingActionsModal({
|
||||
|
||||
{/* Cancel Button */}
|
||||
<TouchableOpacity
|
||||
className="overflow-hidden rounded-2xl bg-white shadow-lg"
|
||||
className="overflow-hidden rounded-2xl bg-white shadow-lg dark:bg-[#171717]"
|
||||
onPress={onClose}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View className="px-4 py-3">
|
||||
<Text className="text-center text-[16px] font-semibold text-gray-700">Cancel</Text>
|
||||
<Text className="text-center text-[16px] font-semibold text-gray-700 dark:text-white">
|
||||
Cancel
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -92,7 +92,7 @@ export function CacheStatusIndicator({
|
||||
if (isFetching) {
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<Ionicons name="sync-outline" size={12} color="#8E8E93" />
|
||||
<Ionicons name="sync-outline" size={12} color="#A3A3A3" />
|
||||
{!compact && <Text style={styles.text}>Updating...</Text>}
|
||||
</View>
|
||||
);
|
||||
@@ -103,14 +103,14 @@ export function CacheStatusIndicator({
|
||||
if (compact) {
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<Ionicons name="time-outline" size={12} color="#8E8E93" />
|
||||
<Ionicons name="time-outline" size={12} color="#A3A3A3" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={[styles.container, style]}>
|
||||
<Ionicons name="time-outline" size={12} color="#8E8E93" />
|
||||
<Ionicons name="time-outline" size={12} color="#A3A3A3" />
|
||||
<Text style={styles.text}>Updated {formatTimeAgo(dataUpdatedAt)}</Text>
|
||||
</View>
|
||||
);
|
||||
@@ -133,7 +133,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
text: {
|
||||
fontSize: 11,
|
||||
color: "#8E8E93",
|
||||
color: "#A3A3A3",
|
||||
},
|
||||
offlineText: {
|
||||
fontSize: 11,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import { Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
|
||||
type IoniconName = keyof typeof Ionicons.glyphMap;
|
||||
|
||||
@@ -20,27 +20,71 @@ export function EmptyScreen({
|
||||
onButtonPress,
|
||||
className,
|
||||
}: EmptyScreenProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
text: isDark ? "#FFFFFF" : "#111827",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
background: isDark ? "#171717" : "#FFFFFF",
|
||||
backgroundSecondary: isDark ? "#2C2C2E" : "#F3F4F6",
|
||||
border: isDark ? "#4D4D4D" : "#D1D5DB",
|
||||
icon: isDark ? "#E5E5EA" : "#374151",
|
||||
buttonBg: isDark ? "#FFFFFF" : "#111827",
|
||||
buttonText: isDark ? "#000000" : "#FFFFFF",
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
className={`flex w-full select-none flex-col items-center justify-center rounded-lg border border-gray-300 bg-white p-7 ${className || ""}`}
|
||||
style={{
|
||||
backgroundColor: colors.background,
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
borderRadius: 8,
|
||||
padding: 28,
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
className={className}
|
||||
>
|
||||
<View className="h-[72px] w-[72px] items-center justify-center rounded-full border border-gray-300 bg-gray-100">
|
||||
<Ionicons name={icon} size={40} color="#374151" />
|
||||
<View
|
||||
style={{
|
||||
width: 72,
|
||||
height: 72,
|
||||
borderRadius: 36,
|
||||
backgroundColor: colors.backgroundSecondary,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name={icon} size={40} color={colors.icon} />
|
||||
</View>
|
||||
|
||||
<View className="mt-6 max-w-[420px] flex-col items-center">
|
||||
<Text className="text-center text-2xl font-semibold text-gray-900">{headline}</Text>
|
||||
<Text style={{ color: colors.text }} className="text-center text-2xl font-semibold">
|
||||
{headline}
|
||||
</Text>
|
||||
|
||||
<Text className="mb-8 mt-3 text-center text-sm font-normal leading-6 text-gray-500">
|
||||
<Text
|
||||
style={{ color: colors.textSecondary }}
|
||||
className="mb-8 mt-3 text-center text-sm font-normal leading-6"
|
||||
>
|
||||
{description}
|
||||
</Text>
|
||||
|
||||
{buttonText && onButtonPress ? (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center gap-1 rounded-lg bg-gray-900 px-4 py-2.5"
|
||||
style={{ backgroundColor: colors.buttonBg }}
|
||||
className="flex-row items-center justify-center gap-1 rounded-lg px-4 py-2.5"
|
||||
onPress={onButtonPress}
|
||||
>
|
||||
<Text className="text-base font-medium text-white">{buttonText}</Text>
|
||||
<Text style={{ color: colors.buttonText }} className="text-base font-medium">
|
||||
{buttonText}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { ActivityIndicator, Text, View } from "react-native";
|
||||
import { ActivityIndicator, Text, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "./AppPressable";
|
||||
|
||||
@@ -24,6 +24,9 @@ export function GlassModalHeader({
|
||||
actionIcon,
|
||||
}: GlassModalHeaderProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const iconColor = isDark ? "#fff" : "#000";
|
||||
|
||||
const isDisabled = actionDisabled || actionLoading;
|
||||
|
||||
@@ -31,8 +34,8 @@ export function GlassModalHeader({
|
||||
<View
|
||||
style={{
|
||||
paddingTop: insets.top,
|
||||
backgroundColor: "#FFFFFF",
|
||||
}}
|
||||
className="bg-white dark:bg-black"
|
||||
>
|
||||
<View className="min-h-[44px] flex-row items-center justify-between px-4">
|
||||
{/* Back Button - matching native iOS style */}
|
||||
@@ -41,11 +44,14 @@ export function GlassModalHeader({
|
||||
onPress={onClose}
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={28} color="#000" />
|
||||
<Ionicons name="chevron-back" size={28} color={iconColor} />
|
||||
</AppPressable>
|
||||
|
||||
{/* Title - centered */}
|
||||
<Text className="flex-1 text-center text-[17px] font-semibold text-black" numberOfLines={1}>
|
||||
<Text
|
||||
className="flex-1 text-center text-[17px] font-semibold text-black dark:text-white"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
@@ -53,7 +59,7 @@ export function GlassModalHeader({
|
||||
{onAction ? (
|
||||
<AppPressable
|
||||
className={`h-11 min-w-[44px] items-center justify-center rounded-full ${
|
||||
actionIcon ? "bg-[#F2F2F7]" : ""
|
||||
actionIcon ? "bg-[#F2F2F7] dark:bg-[#262626]" : ""
|
||||
} ${isDisabled ? "opacity-40" : ""}`}
|
||||
onPress={onAction}
|
||||
disabled={isDisabled}
|
||||
@@ -61,14 +67,16 @@ export function GlassModalHeader({
|
||||
style={actionIcon ? { width: 44, height: 44 } : undefined}
|
||||
>
|
||||
{actionLoading ? (
|
||||
<ActivityIndicator size="small" color="#000" />
|
||||
<ActivityIndicator size="small" color={iconColor} />
|
||||
) : actionIcon ? (
|
||||
// Icon button style (like the three-dot menu)
|
||||
<Ionicons name={actionIcon} size={22} color="#000" />
|
||||
<Ionicons name={actionIcon} size={22} color={iconColor} />
|
||||
) : (
|
||||
// Text button style (like "Save", "Done")
|
||||
<View className="items-center rounded-full bg-black px-4 py-2">
|
||||
<Text className="text-[15px] font-semibold text-white">{actionLabel}</Text>
|
||||
<View className="items-center rounded-full bg-black px-4 py-2 dark:bg-white">
|
||||
<Text className="text-[15px] font-semibold text-white dark:text-black">
|
||||
{actionLabel}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</AppPressable>
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Image, Platform, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
Platform,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { CalComAPIService, type UserProfile } from "@/services/calcom";
|
||||
import { getAvatarUrl } from "@/utils/getAvatarUrl";
|
||||
@@ -53,9 +61,20 @@ export function Header({
|
||||
}: HeaderProps) {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// iOS system colors for dark mode
|
||||
const colors = {
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#666666",
|
||||
background: isDark ? "#000000" : "#FFFFFF",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
logoColor: isDark ? "#FFFFFF" : "#333333",
|
||||
};
|
||||
|
||||
const fetchUserProfile = useCallback(async () => {
|
||||
try {
|
||||
const profile = await CalComAPIService.getUserProfile();
|
||||
@@ -86,12 +105,12 @@ export function Header({
|
||||
|
||||
return (
|
||||
<View
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5EA] bg-white px-2 md:px-4"
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5EA] bg-white px-2 dark:border-[#4D4D4D] dark:bg-black md:px-4"
|
||||
style={{ paddingTop: insets.top + 4, paddingBottom: 4 }}
|
||||
>
|
||||
{/* Left: Cal.com Logo */}
|
||||
<View className="ms-1">
|
||||
<CalComLogo width={101} height={22} color="#333" />
|
||||
<CalComLogo width={101} height={22} color={colors.logoColor} />
|
||||
</View>
|
||||
|
||||
{/* Right: Filter Dropdown + Profile */}
|
||||
@@ -103,14 +122,10 @@ export function Header({
|
||||
{filterOptions && filterOptions.length > 0 && onFilterChange && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<AppPressable
|
||||
className="flex-row items-center gap-1 px-2 py-2"
|
||||
style={{ flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Text className="text-[16px] font-semibold text-[#000000]">
|
||||
<AppPressable className="h-10 flex-row items-center justify-center gap-2 rounded-full border border-[#E5E5E5] bg-white px-4 dark:border-[#262626] dark:bg-[#171717]">
|
||||
<Text className="text-[15px] font-medium text-[#000000] dark:text-white">
|
||||
{activeFilterLabel}
|
||||
</Text>
|
||||
<Ionicons name="chevron-down" size={16} color="#000000" />
|
||||
</AppPressable>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -146,11 +161,13 @@ export function Header({
|
||||
<Ionicons
|
||||
name={isSelected ? "checkmark-circle" : getFilterIcon(option.key)}
|
||||
size={16}
|
||||
color={isSelected ? "#000000" : "#666"}
|
||||
color={isSelected ? colors.text : colors.textSecondary}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
isSelected ? "text-base font-semibold text-[#000000]" : "text-base"
|
||||
isSelected
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base dark:text-gray-300"
|
||||
}
|
||||
>
|
||||
{option.label}
|
||||
@@ -168,14 +185,14 @@ export function Header({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<AppPressable className="relative p-2">
|
||||
<Ionicons name="options-outline" size={22} color="#000000" />
|
||||
<Ionicons name="options-outline" size={22} color={colors.text} />
|
||||
{/* Badge for active filters */}
|
||||
{eventTypeFilterConfig.activeFilterCount > 0 && (
|
||||
<View
|
||||
className="absolute -top-0.5 -right-0.5 items-center justify-center rounded-full bg-[#000000]"
|
||||
className="absolute -top-0.5 -right-0.5 items-center justify-center rounded-full bg-[#000000] dark:bg-white"
|
||||
style={{ width: 16, height: 16 }}
|
||||
>
|
||||
<Text className="text-[10px] font-bold text-white">
|
||||
<Text className="text-[10px] font-bold text-white dark:text-black">
|
||||
{eventTypeFilterConfig.activeFilterCount}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -193,8 +210,8 @@ export function Header({
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Ionicons name="swap-vertical-outline" size={16} color="#666" />
|
||||
<Text className="text-base">Sort by</Text>
|
||||
<Ionicons name="swap-vertical-outline" size={16} color={colors.textSecondary} />
|
||||
<Text className="text-base dark:text-gray-300">Sort by</Text>
|
||||
</View>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent>
|
||||
@@ -209,13 +226,17 @@ export function Header({
|
||||
: "text-outline"
|
||||
}
|
||||
size={16}
|
||||
color={eventTypeFilterConfig.sortBy === "alphabetical" ? "#000000" : "#666"}
|
||||
color={
|
||||
eventTypeFilterConfig.sortBy === "alphabetical"
|
||||
? colors.text
|
||||
: colors.textSecondary
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
eventTypeFilterConfig.sortBy === "alphabetical"
|
||||
? "text-base font-semibold text-[#000000]"
|
||||
: "text-base"
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base dark:text-gray-300"
|
||||
}
|
||||
>
|
||||
Alphabetical
|
||||
@@ -231,13 +252,17 @@ export function Header({
|
||||
: "calendar-outline"
|
||||
}
|
||||
size={16}
|
||||
color={eventTypeFilterConfig.sortBy === "newest" ? "#000000" : "#666"}
|
||||
color={
|
||||
eventTypeFilterConfig.sortBy === "newest"
|
||||
? colors.text
|
||||
: colors.textSecondary
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
eventTypeFilterConfig.sortBy === "newest"
|
||||
? "text-base font-semibold text-[#000000]"
|
||||
: "text-base"
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base dark:text-gray-300"
|
||||
}
|
||||
>
|
||||
Newest First
|
||||
@@ -253,13 +278,17 @@ export function Header({
|
||||
: "time-outline"
|
||||
}
|
||||
size={16}
|
||||
color={eventTypeFilterConfig.sortBy === "duration" ? "#000000" : "#666"}
|
||||
color={
|
||||
eventTypeFilterConfig.sortBy === "duration"
|
||||
? colors.text
|
||||
: colors.textSecondary
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
eventTypeFilterConfig.sortBy === "duration"
|
||||
? "text-base font-semibold text-[#000000]"
|
||||
: "text-base"
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base dark:text-gray-300"
|
||||
}
|
||||
>
|
||||
By Duration
|
||||
@@ -274,8 +303,10 @@ export function Header({
|
||||
{/* Filter Label */}
|
||||
<DropdownMenuLabel>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Ionicons name="filter-outline" size={16} color="#666" />
|
||||
<Text className="text-sm font-medium text-gray-500">Filters</Text>
|
||||
<Ionicons name="filter-outline" size={16} color={colors.textSecondary} />
|
||||
<Text className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||
Filters
|
||||
</Text>
|
||||
</View>
|
||||
</DropdownMenuLabel>
|
||||
|
||||
@@ -289,13 +320,15 @@ export function Header({
|
||||
: "eye-off-outline"
|
||||
}
|
||||
size={16}
|
||||
color={eventTypeFilterConfig.filters.hiddenOnly ? "#000000" : "#666"}
|
||||
color={
|
||||
eventTypeFilterConfig.filters.hiddenOnly ? colors.text : colors.textSecondary
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
eventTypeFilterConfig.filters.hiddenOnly
|
||||
? "text-base font-semibold text-[#000000]"
|
||||
: "text-base"
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base dark:text-gray-300"
|
||||
}
|
||||
>
|
||||
Hidden Only
|
||||
@@ -310,13 +343,15 @@ export function Header({
|
||||
eventTypeFilterConfig.filters.paidOnly ? "checkmark-circle" : "cash-outline"
|
||||
}
|
||||
size={16}
|
||||
color={eventTypeFilterConfig.filters.paidOnly ? "#000000" : "#666"}
|
||||
color={
|
||||
eventTypeFilterConfig.filters.paidOnly ? colors.text : colors.textSecondary
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
eventTypeFilterConfig.filters.paidOnly
|
||||
? "text-base font-semibold text-[#000000]"
|
||||
: "text-base"
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base dark:text-gray-300"
|
||||
}
|
||||
>
|
||||
Paid Events
|
||||
@@ -333,13 +368,15 @@ export function Header({
|
||||
: "people-outline"
|
||||
}
|
||||
size={16}
|
||||
color={eventTypeFilterConfig.filters.seatedOnly ? "#000000" : "#666"}
|
||||
color={
|
||||
eventTypeFilterConfig.filters.seatedOnly ? colors.text : colors.textSecondary
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
eventTypeFilterConfig.filters.seatedOnly
|
||||
? "text-base font-semibold text-[#000000]"
|
||||
: "text-base"
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base dark:text-gray-300"
|
||||
}
|
||||
>
|
||||
Seated Events
|
||||
@@ -359,14 +396,16 @@ export function Header({
|
||||
}
|
||||
size={16}
|
||||
color={
|
||||
eventTypeFilterConfig.filters.requiresConfirmationOnly ? "#000000" : "#666"
|
||||
eventTypeFilterConfig.filters.requiresConfirmationOnly
|
||||
? colors.text
|
||||
: colors.textSecondary
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
eventTypeFilterConfig.filters.requiresConfirmationOnly
|
||||
? "text-base font-semibold text-[#000000]"
|
||||
: "text-base"
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base dark:text-gray-300"
|
||||
}
|
||||
>
|
||||
Requires Confirmation
|
||||
@@ -385,13 +424,17 @@ export function Header({
|
||||
: "repeat-outline"
|
||||
}
|
||||
size={16}
|
||||
color={eventTypeFilterConfig.filters.recurringOnly ? "#000000" : "#666"}
|
||||
color={
|
||||
eventTypeFilterConfig.filters.recurringOnly
|
||||
? colors.text
|
||||
: colors.textSecondary
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
className={
|
||||
eventTypeFilterConfig.filters.recurringOnly
|
||||
? "text-base font-semibold text-[#000000]"
|
||||
: "text-base"
|
||||
? "text-base font-semibold text-[#000000] dark:text-white"
|
||||
: "text-base dark:text-gray-300"
|
||||
}
|
||||
>
|
||||
Recurring
|
||||
@@ -418,7 +461,7 @@ export function Header({
|
||||
{/* Profile Picture */}
|
||||
<TouchableOpacity onPress={handleProfile} className="p-1">
|
||||
{loading ? (
|
||||
<ActivityIndicator size="small" color="#666" />
|
||||
<ActivityIndicator size="small" color={colors.textSecondary} />
|
||||
) : userProfile?.avatarUrl ? (
|
||||
<Image
|
||||
source={{ uri: getAvatarUrl(userProfile.avatarUrl) }}
|
||||
@@ -427,10 +470,10 @@ export function Header({
|
||||
/>
|
||||
) : (
|
||||
<View
|
||||
className="items-center justify-center rounded-full bg-[#E5E5EA]"
|
||||
className="items-center justify-center rounded-full bg-[#E5E5EA] dark:bg-[#4D4D4D]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
>
|
||||
<Ionicons name="person-outline" size={20} color="#666" />
|
||||
<Ionicons name="person-outline" size={20} color={colors.textSecondary} />
|
||||
</View>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -6,7 +6,16 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { Modal, Platform, ScrollView, Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
Modal,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import type { LocationItem, LocationOptionGroup } from "@/types/locations";
|
||||
import {
|
||||
createLocationItemFromOption,
|
||||
@@ -69,29 +78,34 @@ export const AddLocationTrigger = ({
|
||||
loading?: boolean;
|
||||
onPressFallback?: () => void;
|
||||
}) => {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const trigger = isHeader ? (
|
||||
<TouchableOpacity
|
||||
disabled={disabled || loading}
|
||||
onPress={Platform.OS !== "ios" ? onPressFallback : undefined}
|
||||
hitSlop={{ top: 15, bottom: 15, left: 15, right: 15 }}
|
||||
className="flex-row items-center rounded-full border border-[#E5E5E5] bg-[#F2F2F7] p-1"
|
||||
className="flex-row items-center rounded-full border border-[#E5E5E5] bg-[#F2F2F7] p-1 dark:border-[#4D4D4D] dark:bg-[#262626]"
|
||||
>
|
||||
<Ionicons name="add" size={18} color="#000000" />
|
||||
<Ionicons name="add" size={18} color={isDark ? "#FFFFFF" : "#000000"} />
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
disabled={disabled || loading}
|
||||
onPress={Platform.OS !== "ios" ? onPressFallback : undefined}
|
||||
className={`flex-row items-center justify-center rounded-lg border border-dashed border-gray-300 bg-gray-50 px-4 py-3 ${
|
||||
disabled ? "opacity-50" : "active:bg-gray-100"
|
||||
className={`flex-row items-center justify-center rounded-lg border border-dashed border-gray-300 bg-gray-50 px-4 py-3 dark:border-[#4D4D4D] dark:bg-[#262626] ${
|
||||
disabled ? "opacity-50" : "active:bg-gray-100 dark:active:bg-[#171717]"
|
||||
}`}
|
||||
>
|
||||
{loading ? (
|
||||
<Text className="text-sm text-gray-500">Loading options...</Text>
|
||||
<Text className="text-sm text-gray-500 dark:text-[#A3A3A3]">Loading options...</Text>
|
||||
) : (
|
||||
<>
|
||||
<Ionicons name="add-circle-outline" size={20} color="#6B7280" />
|
||||
<Text className="ml-2 text-sm font-medium text-gray-600">Add Location</Text>
|
||||
<Text className="ml-2 text-sm font-medium text-gray-600 dark:text-[#A3A3A3]">
|
||||
Add Location
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
@@ -143,13 +157,13 @@ export const LocationsList: React.FC<LocationsListProps> = ({
|
||||
const renderLocationIcon = (location: LocationItem) => {
|
||||
if (location.iconUrl) {
|
||||
return (
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7]">
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7] dark:bg-[#262626]">
|
||||
<SvgImage uri={location.iconUrl} width={20} height={20} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7]">
|
||||
<View className="mr-3 h-8 w-8 items-center justify-center rounded-lg bg-[#F2F2F7] dark:bg-[#262626]">
|
||||
<Ionicons name="location" size={18} color="#000000" />
|
||||
</View>
|
||||
);
|
||||
@@ -183,9 +197,9 @@ export const LocationsList: React.FC<LocationsListProps> = ({
|
||||
|
||||
return (
|
||||
<View className="mt-2">
|
||||
<Text className="mb-1 text-[13px] text-[#6D6D72]">{label}</Text>
|
||||
<Text className="mb-1 text-[13px] text-[#6D6D72] dark:text-[#A3A3A3]">{label}</Text>
|
||||
<TextInput
|
||||
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
|
||||
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black dark:bg-[#262626] dark:text-white"
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChangeText={(text) => onUpdate(location.id, { [fieldKey]: text })}
|
||||
@@ -198,7 +212,7 @@ export const LocationsList: React.FC<LocationsListProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="bg-white">
|
||||
<View className="bg-white dark:bg-[#171717]">
|
||||
{/* Locations List */}
|
||||
{locations.length > 0 ? (
|
||||
<View className="mb-2">
|
||||
@@ -206,7 +220,9 @@ export const LocationsList: React.FC<LocationsListProps> = ({
|
||||
<View
|
||||
key={location.id}
|
||||
className={`pl-4 ${
|
||||
index !== locations.length - 1 ? "border-b border-[#E5E5E5]" : ""
|
||||
index !== locations.length - 1
|
||||
? "border-b border-[#E5E5E5] dark:border-[#4D4D4D]"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<View
|
||||
@@ -217,7 +233,10 @@ export const LocationsList: React.FC<LocationsListProps> = ({
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="flex-1 flex-row items-center">
|
||||
{renderLocationIcon(location)}
|
||||
<Text className="flex-1 text-[17px] text-black font-normal" numberOfLines={1}>
|
||||
<Text
|
||||
className="flex-1 text-[17px] text-black font-normal dark:text-white"
|
||||
numberOfLines={1}
|
||||
>
|
||||
{location.displayName}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -260,10 +279,12 @@ export const LocationsList: React.FC<LocationsListProps> = ({
|
||||
onRequestClose={() => setShowAddModal(false)}
|
||||
>
|
||||
<View className="flex-1 justify-end bg-black/50">
|
||||
<View className="max-h-[70%] rounded-t-3xl bg-white">
|
||||
<View className="max-h-[70%] rounded-t-3xl bg-white dark:bg-[#171717]">
|
||||
{/* Header */}
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3">
|
||||
<Text className="text-lg font-semibold text-gray-900">Add Location</Text>
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-[#4D4D4D]">
|
||||
<Text className="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
Add Location
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => setShowAddModal(false)} className="p-1">
|
||||
<Ionicons name="close" size={24} color="#6B7280" />
|
||||
</TouchableOpacity>
|
||||
@@ -273,7 +294,7 @@ export const LocationsList: React.FC<LocationsListProps> = ({
|
||||
<ScrollView className="px-4 py-2">
|
||||
{locationOptions.map((group, groupIndex) => (
|
||||
<View key={group.category} className={groupIndex > 0 ? "mt-4" : ""}>
|
||||
<Text className="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500">
|
||||
<Text className="mb-2 text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-[#A3A3A3]">
|
||||
{group.category}
|
||||
</Text>
|
||||
{group.options.map((option) => {
|
||||
@@ -285,7 +306,9 @@ export const LocationsList: React.FC<LocationsListProps> = ({
|
||||
onPress={() => handleSelectOption(option.value, option.label)}
|
||||
disabled={alreadyAdded}
|
||||
className={`flex-row items-center rounded-lg px-2 py-3 ${
|
||||
alreadyAdded ? "opacity-40" : "active:bg-gray-100"
|
||||
alreadyAdded
|
||||
? "opacity-40"
|
||||
: "active:bg-gray-100 dark:active:bg-[#262626]"
|
||||
}`}
|
||||
>
|
||||
{option.iconUrl ? (
|
||||
@@ -300,7 +323,9 @@ export const LocationsList: React.FC<LocationsListProps> = ({
|
||||
<Ionicons name="location-outline" size={24} color="#6B7280" />
|
||||
</View>
|
||||
)}
|
||||
<Text className="flex-1 text-base text-gray-900">{option.label}</Text>
|
||||
<Text className="flex-1 text-base text-gray-900 dark:text-white">
|
||||
{option.label}
|
||||
</Text>
|
||||
{alreadyAdded && <Ionicons name="checkmark" size={20} color="#10B981" />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform, Text, TouchableOpacity, View } from "react-native";
|
||||
import { Platform, Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
@@ -8,6 +8,8 @@ import { CalComLogo } from "./CalComLogo";
|
||||
export function LoginScreen() {
|
||||
const { loginWithOAuth, loading } = useAuth();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const handleOAuthLogin = async () => {
|
||||
try {
|
||||
@@ -28,10 +30,10 @@ export function LoginScreen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-white">
|
||||
<View className="flex-1 bg-white dark:bg-black">
|
||||
{/* Logo centered in the middle */}
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<CalComLogo width={180} height={40} color="#111827" />
|
||||
<CalComLogo width={180} height={40} color={isDark ? "#FFFFFF" : "#111827"} />
|
||||
</View>
|
||||
|
||||
{/* Bottom section with button */}
|
||||
@@ -42,13 +44,13 @@ export function LoginScreen() {
|
||||
disabled={loading}
|
||||
className="flex-row items-center justify-center rounded-2xl py-[18px]"
|
||||
style={[
|
||||
{ backgroundColor: loading ? "#9CA3AF" : "#000000" },
|
||||
{ backgroundColor: loading ? "#9CA3AF" : isDark ? "#FFFFFF" : "#000000" },
|
||||
Platform.select({
|
||||
web: {
|
||||
boxShadow: loading ? "none" : "0 4px 12px rgba(0, 0, 0, 0.2)",
|
||||
},
|
||||
default: {
|
||||
shadowColor: "#000",
|
||||
shadowColor: isDark ? "#FFF" : "#000",
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
shadowOpacity: loading ? 0 : 0.2,
|
||||
shadowRadius: 12,
|
||||
@@ -58,7 +60,12 @@ export function LoginScreen() {
|
||||
]}
|
||||
activeOpacity={0.9}
|
||||
>
|
||||
<Text className="text-[17px] font-semibold text-white">Continue with Cal.com</Text>
|
||||
<Text
|
||||
className="text-[17px] font-semibold"
|
||||
style={{ color: isDark ? "#000000" : "#FFFFFF" }}
|
||||
>
|
||||
Continue with Cal.com
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Sign up link - hidden on iOS */}
|
||||
@@ -70,10 +77,11 @@ export function LoginScreen() {
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View>
|
||||
<Text className="text-[15px] text-gray-500">
|
||||
Don't have an account? <Text className="font-semibold text-gray-900">Sign up</Text>
|
||||
<Text className="text-[15px] text-gray-500 dark:text-[#A3A3A3]">
|
||||
Don't have an account?{" "}
|
||||
<Text className="font-semibold text-gray-900 dark:text-white">Sign up</Text>
|
||||
</Text>
|
||||
<View className="h-px bg-gray-400" style={{ marginTop: 2 }} />
|
||||
<View className="h-px bg-gray-400 dark:bg-[#4D4D4D]" style={{ marginTop: 2 }} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Platform, Text, TouchableOpacity, View } from "react-native";
|
||||
import { Platform, Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import { FullScreenModal } from "./FullScreenModal";
|
||||
|
||||
interface LogoutConfirmModalProps {
|
||||
@@ -8,27 +8,57 @@ interface LogoutConfirmModalProps {
|
||||
}
|
||||
|
||||
export function LogoutConfirmModal({ visible, onConfirm, onCancel }: LogoutConfirmModalProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#111827",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#4B5563",
|
||||
border: isDark ? "#4D4D4D" : "#1F2937",
|
||||
cancelBg: isDark ? "#2C2C2E" : "#FFFFFF",
|
||||
};
|
||||
|
||||
return (
|
||||
<FullScreenModal visible={visible} onRequestClose={onCancel}>
|
||||
<View className="flex-1 items-center justify-center bg-black bg-opacity-50">
|
||||
<View className="mx-4 min-w-[300px] rounded-lg bg-white p-6 shadow-lg">
|
||||
<Text className="mb-2 text-lg font-semibold text-gray-900">Sign Out</Text>
|
||||
<Text className="mb-6 text-gray-600">Are you sure you want to sign out?</Text>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: colors.background,
|
||||
minWidth: 300,
|
||||
borderRadius: 12,
|
||||
padding: 24,
|
||||
}}
|
||||
className="mx-4 shadow-lg"
|
||||
>
|
||||
<Text style={{ color: colors.text }} className="mb-2 text-lg font-semibold">
|
||||
Sign Out
|
||||
</Text>
|
||||
<Text style={{ color: colors.textSecondary }} className="mb-6">
|
||||
Are you sure you want to sign out?
|
||||
</Text>
|
||||
|
||||
<View className="flex-row justify-end space-x-3">
|
||||
<TouchableOpacity
|
||||
onPress={onCancel}
|
||||
className="rounded-md border border-gray-800 bg-white px-4 py-2"
|
||||
style={Platform.OS === "web" ? { cursor: "pointer" } : undefined}
|
||||
style={{
|
||||
backgroundColor: colors.cancelBg,
|
||||
borderColor: colors.border,
|
||||
borderWidth: 1,
|
||||
...(Platform.OS === "web" ? { cursor: "pointer" as const } : {}),
|
||||
}}
|
||||
className="rounded-md px-4 py-2"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text className="font-medium text-gray-800">Cancel</Text>
|
||||
<Text style={{ color: colors.text }} className="font-medium">
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={onConfirm}
|
||||
className="rounded-md border border-red-800 bg-white px-4 py-2"
|
||||
style={Platform.OS === "web" ? { cursor: "pointer" } : undefined}
|
||||
style={Platform.OS === "web" ? { cursor: "pointer" as const } : undefined}
|
||||
className="rounded-md border border-red-800 bg-white px-4 py-2 dark:bg-red-900/30"
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text className="font-medium text-red-800">Sign Out</Text>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type { ReactNode } from "react";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import { Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import { EmptyScreen } from "./EmptyScreen";
|
||||
import { Header } from "./Header";
|
||||
import { LoadingSpinner } from "./LoadingSpinner";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
type IoniconName = keyof typeof Ionicons.glyphMap;
|
||||
|
||||
@@ -68,11 +69,16 @@ export function ScreenWrapper({
|
||||
showHeader = true,
|
||||
children,
|
||||
}: ScreenWrapperProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const errorIconColor = theme.destructive;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<View className="flex-1 bg-gray-100 dark:bg-black">
|
||||
{showHeader && <Header />}
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5 dark:bg-[#171717]">
|
||||
<LoadingSpinner size="large" />
|
||||
</View>
|
||||
</View>
|
||||
@@ -81,17 +87,22 @@ export function ScreenWrapper({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<View className="flex-1 bg-gray-100 dark:bg-black">
|
||||
{showHeader && <Header />}
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<Ionicons name="alert-circle" size={64} color="#800020" />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-gray-800">
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5 dark:bg-[#171717]">
|
||||
<Ionicons name="alert-circle" size={64} color={errorIconColor} />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-gray-800 dark:text-gray-100">
|
||||
{errorTitle}
|
||||
</Text>
|
||||
<Text className="mb-6 text-center text-base text-gray-500">{error}</Text>
|
||||
<Text className="mb-6 text-center text-base text-gray-500 dark:text-gray-400">
|
||||
{error}
|
||||
</Text>
|
||||
{onRetry && (
|
||||
<TouchableOpacity className="rounded-lg bg-black px-6 py-3" onPress={onRetry}>
|
||||
<Text className="text-base font-semibold text-white">Retry</Text>
|
||||
<TouchableOpacity
|
||||
className="rounded-lg bg-black px-6 py-3 dark:bg-white"
|
||||
onPress={onRetry}
|
||||
>
|
||||
<Text className="text-base font-semibold text-white dark:text-black">Retry</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
@@ -101,9 +112,9 @@ export function ScreenWrapper({
|
||||
|
||||
if (isEmpty && emptyProps) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<View className="flex-1 bg-gray-100 dark:bg-black">
|
||||
{showHeader && <Header />}
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5 dark:bg-[#171717]">
|
||||
<EmptyScreen {...emptyProps} />
|
||||
</View>
|
||||
</View>
|
||||
@@ -111,7 +122,7 @@ export function ScreenWrapper({
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<View className="flex-1 bg-gray-100 dark:bg-black">
|
||||
{showHeader && <Header />}
|
||||
{children}
|
||||
</View>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { Text, TextInput, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
|
||||
interface SearchHeaderProps {
|
||||
/** Current search query value */
|
||||
@@ -35,10 +35,13 @@ export function SearchHeader({
|
||||
onNewPress,
|
||||
newButtonText = "New",
|
||||
}: SearchHeaderProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<View className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2">
|
||||
<View className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2 dark:border-[#4D4D4D] dark:bg-black">
|
||||
<TextInput
|
||||
className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-[17px] text-black focus:border-black focus:ring-2 focus:ring-black"
|
||||
className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-[17px] text-black focus:border-black focus:ring-2 focus:ring-black dark:border-[#4D4D4D] dark:bg-[#171717] dark:text-white"
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={searchQuery}
|
||||
@@ -47,11 +50,11 @@ export function SearchHeader({
|
||||
autoCorrect={false}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
|
||||
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2 dark:bg-white"
|
||||
onPress={onNewPress}
|
||||
>
|
||||
<Ionicons name="add" size={18} color="#fff" />
|
||||
<Text className="text-base font-semibold text-white">{newButtonText}</Text>
|
||||
<Ionicons name="add" size={18} color={isDark ? "#000" : "#fff"} />
|
||||
<Text className="text-base font-semibold text-white dark:text-black">{newButtonText}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
|
||||
import { buttonStyle, frame } from "@expo/ui/swift-ui/modifiers";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Pressable, useColorScheme, View } from "react-native";
|
||||
import type { SFSymbols7_0 } from "sf-symbols-typescript";
|
||||
import type { AvailabilityListItemProps } from "./AvailabilityListItem";
|
||||
import { AvailabilitySlots, ScheduleName, TimeZoneRow } from "./AvailabilityListItemParts";
|
||||
@@ -43,8 +43,17 @@ export const AvailabilityListItem = ({
|
||||
},
|
||||
];
|
||||
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<View className="border-b border-cal-border bg-cal-bg">
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isDark ? "#000000" : "#FFFFFF",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
}}
|
||||
>
|
||||
{/* Native iOS Context Menu for long-press */}
|
||||
<Host matchContents>
|
||||
<ContextMenu
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Pressable, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { Text } from "@/components/ui/text";
|
||||
import type { Schedule } from "@/hooks";
|
||||
import { AvailabilitySlots, ScheduleName, TimeZoneRow } from "./AvailabilityListItemParts";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
export interface AvailabilityListItemProps {
|
||||
item: Schedule;
|
||||
@@ -38,6 +39,10 @@ export const AvailabilityListItem = ({
|
||||
right: 12,
|
||||
};
|
||||
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
type DropdownAction = {
|
||||
label: string;
|
||||
icon: keyof typeof Ionicons.glyphMap;
|
||||
@@ -83,7 +88,13 @@ export const AvailabilityListItem = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="border-b border-cal-border bg-cal-bg">
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isDark ? "#000000" : "#FFFFFF",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className="flex-row items-center"
|
||||
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
||||
@@ -105,10 +116,22 @@ export const AvailabilityListItem = ({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Pressable
|
||||
className="items-center justify-center rounded-lg border border-cal-border"
|
||||
style={{ width: 32, height: 32, flexShrink: 0 }}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
flexShrink: 0,
|
||||
borderWidth: 1,
|
||||
borderColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
<Ionicons
|
||||
name="ellipsis-horizontal"
|
||||
size={18}
|
||||
color={isDark ? "#E5E5EA" : "#3C3F44"}
|
||||
/>
|
||||
</Pressable>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -122,10 +145,20 @@ export const AvailabilityListItem = ({
|
||||
<Ionicons
|
||||
name={action.icon}
|
||||
size={18}
|
||||
color={action.variant === "destructive" ? "#800020" : "#374151"}
|
||||
color={
|
||||
action.variant === "destructive"
|
||||
? theme.destructive
|
||||
: isDark
|
||||
? "#E5E5EA"
|
||||
: "#374151"
|
||||
}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text className={action.variant === "destructive" ? "text-destructive" : ""}>
|
||||
<Text
|
||||
style={
|
||||
action.variant === "destructive" ? { color: theme.destructive } : undefined
|
||||
}
|
||||
>
|
||||
{action.label}
|
||||
</Text>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import { Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import type { Schedule } from "@/hooks";
|
||||
|
||||
// Convert 24-hour time to 12-hour format with AM/PM
|
||||
@@ -18,11 +18,19 @@ interface ScheduleNameProps {
|
||||
}
|
||||
|
||||
export function ScheduleName({ name, isDefault }: ScheduleNameProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<View className="mb-1 flex-row flex-wrap items-center">
|
||||
<Text className="text-base font-semibold text-cal-text">{name}</Text>
|
||||
<Text style={{ color: isDark ? "#FFFFFF" : "#333333" }} className="text-base font-semibold">
|
||||
{name}
|
||||
</Text>
|
||||
{isDefault && (
|
||||
<View className="ml-2 rounded bg-cal-text-secondary px-2 py-0.5">
|
||||
<View
|
||||
className="ml-2 rounded px-2 py-0.5"
|
||||
style={{ backgroundColor: isDark ? "#A3A3A3" : "#666666" }}
|
||||
>
|
||||
<Text className="text-xs font-semibold text-white">Default</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -36,8 +44,16 @@ interface AvailabilitySlotsProps {
|
||||
}
|
||||
|
||||
export function AvailabilitySlots({ availability, scheduleId }: AvailabilitySlotsProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const textColor = isDark ? "#A3A3A3" : "#666666";
|
||||
|
||||
if (!availability || availability.length === 0) {
|
||||
return <Text className="text-sm text-cal-text-secondary">No availability set</Text>;
|
||||
return (
|
||||
<Text style={{ color: textColor }} className="text-sm">
|
||||
No availability set
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_VISIBLE_SLOTS = 2;
|
||||
@@ -51,14 +67,14 @@ export function AvailabilitySlots({ availability, scheduleId }: AvailabilitySlot
|
||||
key={`${scheduleId}-${slot.days.join("-")}-${slotIndex}`}
|
||||
className={slotIndex > 0 ? "mt-1" : ""}
|
||||
>
|
||||
<Text className="text-sm text-cal-text-secondary" numberOfLines={1}>
|
||||
<Text style={{ color: textColor }} className="text-sm" numberOfLines={1}>
|
||||
{slot.days.join(", ")} {formatTime12Hour(slot.startTime)} -{" "}
|
||||
{formatTime12Hour(slot.endTime)}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
{remainingCount > 0 && (
|
||||
<Text className="mt-1 text-sm text-cal-text-secondary">
|
||||
<Text style={{ color: textColor }} className="mt-1 text-sm">
|
||||
+{remainingCount} more {remainingCount === 1 ? "slot" : "slots"}
|
||||
</Text>
|
||||
)}
|
||||
@@ -71,10 +87,16 @@ interface TimeZoneRowProps {
|
||||
}
|
||||
|
||||
export function TimeZoneRow({ timeZone }: TimeZoneRowProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const iconColor = isDark ? "#A3A3A3" : "#666666";
|
||||
|
||||
return (
|
||||
<View className="mt-2 flex-row items-center">
|
||||
<Ionicons name="globe-outline" size={14} color="#666666" />
|
||||
<Text className="ml-1.5 text-sm text-cal-text-secondary">{timeZone}</Text>
|
||||
<Ionicons name="globe-outline" size={14} color={iconColor} />
|
||||
<Text style={{ color: iconColor }} className="ml-1.5 text-sm">
|
||||
{timeZone}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -90,17 +112,27 @@ export function ScheduleActionsButton({
|
||||
setSelectedSchedule,
|
||||
setShowActionsModal,
|
||||
}: ScheduleActionsButtonProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-lg border border-cal-border"
|
||||
style={{ width: 32, height: 32 }}
|
||||
style={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderWidth: 1,
|
||||
borderColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedSchedule(schedule);
|
||||
setShowActionsModal(true);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color={isDark ? "#E5E5EA" : "#3C3F44"} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { ScrollView, useColorScheme, View } from "react-native";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface AvailabilityListItemSkeletonProps {
|
||||
isLast?: boolean;
|
||||
@@ -8,8 +9,18 @@ interface AvailabilityListItemSkeletonProps {
|
||||
export function AvailabilityListItemSkeleton({
|
||||
isLast = false,
|
||||
}: AvailabilityListItemSkeletonProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
return (
|
||||
<View className={`bg-cal-bg ${!isLast ? "border-b border-cal-border" : ""}`}>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.background,
|
||||
borderBottomWidth: !isLast ? 1 : 0,
|
||||
borderBottomColor: theme.border,
|
||||
}}
|
||||
>
|
||||
<View style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }}>
|
||||
<View
|
||||
style={{
|
||||
@@ -59,10 +70,14 @@ export function AvailabilityListSkeleton({
|
||||
count = 4,
|
||||
iosStyle = false,
|
||||
}: AvailabilityListSkeletonProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
if (iosStyle) {
|
||||
return (
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: theme.background }}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
@@ -71,7 +86,7 @@ export function AvailabilityListSkeleton({
|
||||
flex: 1,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: "#E5E5EA",
|
||||
borderColor: theme.border,
|
||||
marginHorizontal: 8,
|
||||
marginVertical: 4,
|
||||
overflow: "hidden",
|
||||
@@ -91,8 +106,15 @@ export function AvailabilityListSkeleton({
|
||||
|
||||
return (
|
||||
<View
|
||||
className="flex-1 rounded-lg border border-[#E5E5EA] bg-white"
|
||||
style={{ paddingHorizontal: 8, paddingVertical: 4 }}
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
<AvailabilityListItemSkeleton
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
|
||||
import { buttonStyle, frame } from "@expo/ui/swift-ui/modifiers";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import React from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Pressable, useColorScheme, View } from "react-native";
|
||||
import type { SFSymbols7_0 } from "sf-symbols-typescript";
|
||||
import { getBookingActions } from "@/utils/booking-actions";
|
||||
import {
|
||||
@@ -57,6 +57,9 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
});
|
||||
}, [booking, userEmail]);
|
||||
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
// Define context menu actions based on booking state
|
||||
type ContextMenuAction = {
|
||||
label: string;
|
||||
@@ -136,7 +139,13 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
.map(({ label, icon, onPress, role }) => ({ label, icon, onPress, role }));
|
||||
|
||||
return (
|
||||
<View className="border-b border-cal-border bg-cal-bg">
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isDark ? "#000000" : "#FFFFFF",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
}}
|
||||
>
|
||||
{/* Native iOS Context Menu for long-press */}
|
||||
<Host matchContents>
|
||||
<ContextMenu
|
||||
@@ -157,8 +166,12 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
<ContextMenu.Trigger>
|
||||
<Pressable
|
||||
onPress={() => onPress(booking)}
|
||||
style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }}
|
||||
className="active:bg-cal-bg-secondary"
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 12,
|
||||
}}
|
||||
className="active:bg-cal-bg-secondary dark:active:bg-[#171717]"
|
||||
>
|
||||
<TimeAndDateRow
|
||||
formattedDate={formattedDate}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { Pressable, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Text } from "@/components/ui/text";
|
||||
import { getBookingActions } from "@/utils/booking-actions";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import {
|
||||
BadgesRow,
|
||||
BookingDescription,
|
||||
@@ -40,6 +41,17 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
onReportBooking,
|
||||
onCancelBooking,
|
||||
}) => {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const [isPressed, setIsPressed] = useState(false);
|
||||
|
||||
const colors = {
|
||||
icon: isDark ? "#FFFFFF" : "#3C3F44",
|
||||
iconDestructive: theme.destructive,
|
||||
iconDefault: isDark ? "#E5E5EA" : "#374151",
|
||||
};
|
||||
|
||||
const {
|
||||
isUpcoming,
|
||||
isPending,
|
||||
@@ -148,12 +160,24 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="border-b border-cal-border bg-cal-bg">
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isDark ? "#000000" : "#FFFFFF",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => onPress(booking)}
|
||||
style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }}
|
||||
className="active:bg-cal-bg-secondary"
|
||||
android_ripple={{ color: "rgba(0, 0, 0, 0.1)" }}
|
||||
onPressIn={() => setIsPressed(true)}
|
||||
onPressOut={() => setIsPressed(false)}
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 12,
|
||||
backgroundColor: isPressed ? (isDark ? "#171717" : "#F3F4F6") : "transparent",
|
||||
}}
|
||||
android_ripple={{ color: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)" }}
|
||||
>
|
||||
<TimeAndDateRow formattedDate={formattedDate} formattedTimeRange={formattedTimeRange} />
|
||||
<BadgesRow isPending={isPending} />
|
||||
@@ -182,10 +206,10 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Pressable
|
||||
className="items-center justify-center rounded-lg border border-cal-border"
|
||||
className="items-center justify-center rounded-lg border border-cal-border dark:border-cal-border-dark"
|
||||
style={{ width: 32, height: 32 }}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color={colors.icon} />
|
||||
</Pressable>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -199,10 +223,23 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
<Ionicons
|
||||
name={action.icon}
|
||||
size={18}
|
||||
color={action.variant === "destructive" ? "#800020" : "#374151"}
|
||||
color={
|
||||
action.variant === "destructive"
|
||||
? colors.iconDestructive
|
||||
: colors.iconDefault
|
||||
}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text className={action.variant === "destructive" ? "text-destructive" : ""}>
|
||||
<Text
|
||||
style={{
|
||||
color:
|
||||
action.variant === "destructive"
|
||||
? colors.iconDestructive
|
||||
: isDark
|
||||
? "#FFFFFF"
|
||||
: "#374151",
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</Text>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Linking, Text, TouchableOpacity, View } from "react-native";
|
||||
import { Linking, Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import { SvgImage } from "@/components/SvgImage";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import type { BookingListItemData } from "./useBookingListItemData";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface TimeAndDateRowProps {
|
||||
formattedDate: string;
|
||||
@@ -13,8 +14,10 @@ interface TimeAndDateRowProps {
|
||||
export function TimeAndDateRow({ formattedDate, formattedTimeRange }: TimeAndDateRowProps) {
|
||||
return (
|
||||
<View className="mb-2 flex-row flex-wrap items-center">
|
||||
<Text className="text-sm font-medium text-cal-text">{formattedDate}</Text>
|
||||
<Text className="ml-2 text-sm text-cal-text-secondary">{formattedTimeRange}</Text>
|
||||
<Text className="text-sm font-medium text-cal-text dark:text-white">{formattedDate}</Text>
|
||||
<Text className="ml-2 text-sm text-cal-text-secondary dark:text-[#A3A3A3]">
|
||||
{formattedTimeRange}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -44,7 +47,7 @@ interface BookingTitleProps {
|
||||
export function BookingTitle({ title, isCancelled, isRejected }: BookingTitleProps) {
|
||||
return (
|
||||
<Text
|
||||
className={`mb-2 text-lg font-medium leading-5 text-cal-text ${isCancelled || isRejected ? "line-through" : ""}`}
|
||||
className={`mb-2 text-lg font-medium leading-5 text-cal-text dark:text-white ${isCancelled || isRejected ? "line-through" : ""}`}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{title}
|
||||
@@ -59,7 +62,10 @@ interface BookingDescriptionProps {
|
||||
export function BookingDescription({ description }: BookingDescriptionProps) {
|
||||
if (!description) return null;
|
||||
return (
|
||||
<Text className="mb-2 text-sm leading-5 text-cal-text-secondary" numberOfLines={1}>
|
||||
<Text
|
||||
className="mb-2 text-sm leading-5 text-cal-text-secondary dark:text-[#A3A3A3]"
|
||||
numberOfLines={1}
|
||||
>
|
||||
"{description}"
|
||||
</Text>
|
||||
);
|
||||
@@ -74,13 +80,16 @@ export function HostAndAttendees({
|
||||
hostAndAttendeesDisplay,
|
||||
hasNoShowAttendee,
|
||||
}: HostAndAttendeesProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
|
||||
if (!hostAndAttendeesDisplay) return null;
|
||||
return (
|
||||
<View className="mb-2 flex-row items-center">
|
||||
<Text className="text-sm text-cal-text">{hostAndAttendeesDisplay}</Text>
|
||||
<Text className="text-sm text-cal-text dark:text-white">{hostAndAttendeesDisplay}</Text>
|
||||
{hasNoShowAttendee && (
|
||||
<View className="ml-2 flex-row items-center rounded-full bg-[#FEE2E2] px-1.5 py-0.5">
|
||||
<Ionicons name="eye-off" size={10} color="#800020" />
|
||||
<View className="ml-2 flex-row items-center rounded-full bg-[#FEE2E2] px-1.5 py-0.5 dark:bg-red-900/30">
|
||||
<Ionicons name="eye-off" size={10} color={theme.destructive} />
|
||||
<Text className="ml-0.5 text-[10px] font-medium text-cal-accent-destructive">
|
||||
No-show
|
||||
</Text>
|
||||
@@ -139,27 +148,19 @@ export function ConfirmRejectButtons({
|
||||
onConfirm,
|
||||
onReject,
|
||||
}: ConfirmRejectButtonsProps) {
|
||||
if (!isPending) return null;
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const iconColor = isDark ? "#FFFFFF" : "#3C3F44";
|
||||
|
||||
// Check both endTime and end (optional) properties
|
||||
const endDateStr = booking.endTime || booking.end;
|
||||
const isPast = endDateStr ? new Date(endDateStr) < new Date() : false;
|
||||
|
||||
if (!isPending || isPast) return null;
|
||||
return (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border border-cal-border bg-cal-bg"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
opacity: isConfirming || isDeclining ? 0.5 : 1,
|
||||
}}
|
||||
disabled={isConfirming || isDeclining}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
onConfirm(booking);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark" size={16} color="#3C3F44" />
|
||||
<Text className="ml-1 text-sm font-medium text-cal-text-emphasis">Confirm</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border border-cal-border bg-cal-bg"
|
||||
className="flex-row items-center justify-center rounded-lg border border-cal-border bg-white dark:border-cal-border-dark dark:bg-[#171717]"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
@@ -171,8 +172,26 @@ export function ConfirmRejectButtons({
|
||||
onReject(booking);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={16} color="#3C3F44" />
|
||||
<Text className="ml-1 text-sm font-medium text-cal-text-emphasis">Reject</Text>
|
||||
<Ionicons name="close" size={16} color={iconColor} />
|
||||
<Text className="ml-1 text-sm font-medium text-cal-text-emphasis dark:text-white">
|
||||
Reject
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg bg-black dark:bg-white"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
opacity: isConfirming || isDeclining ? 0.5 : 1,
|
||||
}}
|
||||
disabled={isConfirming || isDeclining}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
onConfirm(booking);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark" size={16} color={isDark ? "#000000" : "#FFFFFF"} />
|
||||
<Text className="ml-1 text-sm font-medium text-white dark:text-black">Confirm</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { ScrollView, View, useColorScheme } from "react-native";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface BookingListItemSkeletonProps {
|
||||
isLast?: boolean;
|
||||
}
|
||||
|
||||
export function BookingListItemSkeleton({ isLast = false }: BookingListItemSkeletonProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
return (
|
||||
<View className={`bg-cal-bg ${!isLast ? "border-b border-cal-border" : ""}`}>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.background,
|
||||
borderBottomWidth: !isLast ? 1 : 0,
|
||||
borderBottomColor: theme.border,
|
||||
}}
|
||||
>
|
||||
<View style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }}>
|
||||
<View
|
||||
style={{ flexDirection: "row", flexWrap: "wrap", alignItems: "center", marginBottom: 8 }}
|
||||
@@ -53,18 +64,22 @@ interface BookingListSkeletonProps {
|
||||
}
|
||||
|
||||
export function BookingListSkeleton({ count = 4, iosStyle = false }: BookingListSkeletonProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
if (iosStyle) {
|
||||
return (
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: theme.background }}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#f1f1f1",
|
||||
backgroundColor: theme.backgroundMuted,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#E5E5EA",
|
||||
borderBottomColor: theme.border,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 12,
|
||||
}}
|
||||
@@ -81,12 +96,15 @@ export function BookingListSkeleton({ count = 4, iosStyle = false }: BookingList
|
||||
|
||||
return (
|
||||
<View className="flex-1 px-2 pt-4 md:px-4">
|
||||
<View className="flex-1 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
className="flex-1 overflow-hidden rounded-lg border"
|
||||
style={{ borderColor: theme.border, backgroundColor: theme.background }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#f1f1f1",
|
||||
backgroundColor: theme.backgroundMuted,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#E5E5EA",
|
||||
borderBottomColor: theme.border,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 12,
|
||||
}}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { buttonStyle, frame } from "@expo/ui/swift-ui/modifiers";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { Pressable, Text, TouchableOpacity, View, Linking } from "react-native";
|
||||
import { Pressable, Text, TouchableOpacity, View, Linking, useColorScheme } from "react-native";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import type { SFSymbols7_0 } from "sf-symbols-typescript";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import type { RecurringBookingGroup } from "@/utils/bookings-utils";
|
||||
@@ -81,6 +82,10 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
});
|
||||
}, [booking, userEmail]);
|
||||
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
// Define context menu actions based on booking state
|
||||
type ContextMenuAction = {
|
||||
label: string;
|
||||
@@ -160,16 +165,24 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
.map(({ label, icon, onPress, role }) => ({ label, icon, onPress, role }));
|
||||
|
||||
return (
|
||||
<View className="border-b border-cal-border bg-cal-bg">
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isDark ? "#000000" : "#FFFFFF",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => onPress(group)}
|
||||
style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }}
|
||||
className="active:bg-cal-bg-secondary"
|
||||
className="active:bg-cal-bg-secondary dark:active:bg-[#171717]"
|
||||
>
|
||||
{/* Date and Time */}
|
||||
<View className="mb-2 flex-row flex-wrap items-center">
|
||||
<Text className="text-sm font-medium text-cal-text">{formattedDate}</Text>
|
||||
<Text className="ml-2 text-sm text-cal-text-secondary">{formattedTimeRange}</Text>
|
||||
<Text className="text-sm font-medium text-cal-text dark:text-white">{formattedDate}</Text>
|
||||
<Text className="ml-2 text-sm text-cal-text-secondary dark:text-[#A3A3A3]">
|
||||
{formattedTimeRange}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Badges Row */}
|
||||
@@ -183,7 +196,14 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
|
||||
{/* Unconfirmed Badge */}
|
||||
{group.hasUnconfirmed && (
|
||||
<View className="rounded bg-cal-accent-warning px-2 py-0.5">
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#FF9500", // cal-accent-warning
|
||||
borderRadius: 4,
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 2,
|
||||
}}
|
||||
>
|
||||
<Text className="text-xs font-medium text-white">Unconfirmed</Text>
|
||||
</View>
|
||||
)}
|
||||
@@ -191,12 +211,14 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
|
||||
{/* Recurrence Pattern Text (for unconfirmed recurring) */}
|
||||
{group.hasUnconfirmed && group.recurrenceText && (
|
||||
<Text className="mb-2 text-sm text-cal-text-secondary">{group.recurrenceText}</Text>
|
||||
<Text className="mb-2 text-sm text-cal-text-secondary dark:text-[#A3A3A3]">
|
||||
{group.recurrenceText}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<Text
|
||||
className={`mb-2 text-lg font-medium leading-5 text-cal-text ${isCancelled || isRejected ? "line-through" : ""}`}
|
||||
className={`mb-2 text-lg font-medium leading-5 text-cal-text dark:text-white ${isCancelled || isRejected ? "line-through" : ""}`}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.title}
|
||||
@@ -204,7 +226,10 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
|
||||
{/* Description */}
|
||||
{booking.description ? (
|
||||
<Text className="mb-2 text-sm leading-5 text-cal-text-secondary" numberOfLines={1}>
|
||||
<Text
|
||||
className="mb-2 text-sm leading-5 text-cal-text-secondary dark:text-[#A3A3A3]"
|
||||
numberOfLines={1}
|
||||
>
|
||||
"{booking.description}"
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -212,7 +237,7 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
{/* Host and Attendees */}
|
||||
{hostAndAttendeesDisplay ? (
|
||||
<View className="mb-2 flex-row items-center">
|
||||
<Text className="text-sm text-cal-text">{hostAndAttendeesDisplay}</Text>
|
||||
<Text className="text-sm text-cal-text dark:text-white">{hostAndAttendeesDisplay}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -256,7 +281,7 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
{/* Confirm All / Reject All for unconfirmed recurring */}
|
||||
{group.hasUnconfirmed && onRejectAll && (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border border-cal-border bg-cal-bg"
|
||||
className="flex-row items-center justify-center rounded-lg border border-cal-border bg-white dark:border-cal-border-dark dark:bg-[#171717]"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 32,
|
||||
@@ -268,14 +293,16 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
onRejectAll(group);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={16} color="#3C3F44" />
|
||||
<Text className="ml-1 text-sm font-medium text-cal-text-emphasis">Reject all</Text>
|
||||
<Ionicons name="close" size={16} color={isDark ? "#FFFFFF" : "#3C3F44"} />
|
||||
<Text className="ml-1 text-sm font-medium text-cal-text-emphasis dark:text-white">
|
||||
Reject all
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{group.hasUnconfirmed && onConfirmAll && (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg bg-black"
|
||||
className="flex-row items-center justify-center rounded-lg bg-black dark:bg-white"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 32,
|
||||
@@ -287,20 +314,20 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
onConfirmAll(group);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark" size={16} color="#FFFFFF" />
|
||||
<Text className="ml-1 text-sm font-medium text-white">Confirm all</Text>
|
||||
<Ionicons name="checkmark" size={16} color={isDark ? "#000000" : "#FFFFFF"} />
|
||||
<Text className="ml-1 text-sm font-medium text-white dark:text-black">Confirm all</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Cancel All Remaining */}
|
||||
{onCancelAllRemaining && group.remainingCount > 0 && !group.hasUnconfirmed && (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border bg-cal-bg"
|
||||
className="flex-row items-center justify-center rounded-lg border bg-white dark:bg-[#171717]"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 32,
|
||||
opacity: isProcessing ? 0.5 : 1,
|
||||
borderColor: "#800020",
|
||||
borderColor: theme.destructive,
|
||||
}}
|
||||
disabled={isProcessing}
|
||||
onPress={(e) => {
|
||||
@@ -308,8 +335,8 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
onCancelAllRemaining(group);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close-circle-outline" size={16} color="#800020" />
|
||||
<Text className="ml-1 text-sm font-medium" style={{ color: "#800020" }}>
|
||||
<Ionicons name="close-circle-outline" size={16} color={theme.destructive} />
|
||||
<Text className="ml-1 text-sm font-medium" style={{ color: theme.destructive }}>
|
||||
Cancel all remaining
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { Pressable, Text, TouchableOpacity, View } from "react-native";
|
||||
import { Pressable, Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Text as UIText } from "@/components/ui/text";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import type { RecurringBookingGroup } from "@/utils/bookings-utils";
|
||||
import { formatDate, formatTime, getHostAndAttendeesDisplay } from "@/utils/bookings-utils";
|
||||
@@ -58,6 +59,16 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
onReportBooking,
|
||||
onCancelBooking,
|
||||
}) => {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const colors = {
|
||||
icon: isDark ? "#FFFFFF" : "#3C3F44",
|
||||
iconDestructive: theme.destructive,
|
||||
iconDefault: isDark ? "#E5E5EA" : "#374151",
|
||||
};
|
||||
|
||||
const booking = group.firstUpcoming;
|
||||
const startTime = booking.start || booking.startTime || "";
|
||||
const endTime = booking.end || booking.endTime || "";
|
||||
@@ -169,17 +180,25 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
);
|
||||
|
||||
return (
|
||||
<View className="border-b border-cal-border bg-cal-bg">
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isDark ? "#000000" : "#FFFFFF",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => onPress(group)}
|
||||
style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }}
|
||||
className="active:bg-cal-bg-secondary"
|
||||
android_ripple={{ color: "rgba(0, 0, 0, 0.1)" }}
|
||||
className="active:bg-cal-bg-secondary dark:active:bg-[#171717]"
|
||||
android_ripple={{ color: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)" }}
|
||||
>
|
||||
{/* Date and Time */}
|
||||
<View className="mb-2 flex-row flex-wrap items-center">
|
||||
<Text className="text-sm font-medium text-cal-text">{formattedDate}</Text>
|
||||
<Text className="ml-2 text-sm text-cal-text-secondary">{formattedTimeRange}</Text>
|
||||
<Text className="text-sm font-medium text-cal-text dark:text-white">{formattedDate}</Text>
|
||||
<Text className="ml-2 text-sm text-cal-text-secondary dark:text-[#A3A3A3]">
|
||||
{formattedTimeRange}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Badges Row */}
|
||||
@@ -201,12 +220,14 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
|
||||
{/* Recurrence Pattern Text (for unconfirmed recurring) */}
|
||||
{group.hasUnconfirmed && group.recurrenceText && (
|
||||
<Text className="mb-2 text-sm text-cal-text-secondary">{group.recurrenceText}</Text>
|
||||
<Text className="mb-2 text-sm text-cal-text-secondary dark:text-[#A3A3A3]">
|
||||
{group.recurrenceText}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Title */}
|
||||
<Text
|
||||
className={`mb-2 text-lg font-medium leading-5 text-cal-text ${isCancelled || isRejected ? "line-through" : ""}`}
|
||||
className={`mb-2 text-lg font-medium leading-5 text-cal-text dark:text-white ${isCancelled || isRejected ? "line-through" : ""}`}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.title}
|
||||
@@ -214,7 +235,10 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
|
||||
{/* Description */}
|
||||
{booking.description ? (
|
||||
<Text className="mb-2 text-sm leading-5 text-cal-text-secondary" numberOfLines={1}>
|
||||
<Text
|
||||
className="mb-2 text-sm leading-5 text-cal-text-secondary dark:text-[#A3A3A3]"
|
||||
numberOfLines={1}
|
||||
>
|
||||
"{booking.description}"
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -222,7 +246,7 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
{/* Host and Attendees */}
|
||||
{hostAndAttendeesDisplay ? (
|
||||
<View className="mb-2 flex-row items-center">
|
||||
<Text className="text-sm text-cal-text">{hostAndAttendeesDisplay}</Text>
|
||||
<Text className="text-sm text-cal-text dark:text-white">{hostAndAttendeesDisplay}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -266,7 +290,7 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
{/* Confirm All / Reject All for unconfirmed recurring */}
|
||||
{group.hasUnconfirmed && onRejectAll && (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border border-cal-border bg-cal-bg"
|
||||
className="flex-row items-center justify-center rounded-lg border border-cal-border bg-white dark:border-cal-border-dark dark:bg-[#171717]"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 32,
|
||||
@@ -278,14 +302,16 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
onRejectAll(group);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={16} color="#3C3F44" />
|
||||
<Text className="ml-1 text-sm font-medium text-cal-text-emphasis">Reject all</Text>
|
||||
<Ionicons name="close" size={16} color={isDark ? "#FFFFFF" : "#3C3F44"} />
|
||||
<Text className="ml-1 text-sm font-medium text-cal-text-emphasis dark:text-white">
|
||||
Reject all
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{group.hasUnconfirmed && onConfirmAll && (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg bg-black"
|
||||
className="flex-row items-center justify-center rounded-lg bg-black dark:bg-white"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 32,
|
||||
@@ -297,20 +323,20 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
onConfirmAll(group);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark" size={16} color="#FFFFFF" />
|
||||
<Text className="ml-1 text-sm font-medium text-white">Confirm all</Text>
|
||||
<Ionicons name="checkmark" size={16} color={isDark ? "#000000" : "#FFFFFF"} />
|
||||
<Text className="ml-1 text-sm font-medium text-white dark:text-black">Confirm all</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
{/* Cancel All Remaining */}
|
||||
{onCancelAllRemaining && group.remainingCount > 0 && !group.hasUnconfirmed && (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border bg-cal-bg"
|
||||
className="flex-row items-center justify-center rounded-lg border bg-white dark:bg-[#171717]"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
height: 32,
|
||||
opacity: isProcessing ? 0.5 : 1,
|
||||
borderColor: "#800020",
|
||||
borderColor: theme.destructive,
|
||||
}}
|
||||
disabled={isProcessing}
|
||||
onPress={(e) => {
|
||||
@@ -318,8 +344,8 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
onCancelAllRemaining(group);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close-circle-outline" size={16} color="#800020" />
|
||||
<Text className="ml-1 text-sm font-medium" style={{ color: "#800020" }}>
|
||||
<Ionicons name="close-circle-outline" size={16} color={theme.destructive} />
|
||||
<Text className="ml-1 text-sm font-medium" style={{ color: theme.destructive }}>
|
||||
Cancel all remaining
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
@@ -330,10 +356,10 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Pressable
|
||||
className="items-center justify-center rounded-lg border border-cal-border"
|
||||
className="items-center justify-center rounded-lg border border-cal-border dark:border-cal-border-dark"
|
||||
style={{ width: 32, height: 32 }}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color={colors.icon} />
|
||||
</Pressable>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -347,10 +373,23 @@ export const RecurringBookingListItem: React.FC<RecurringBookingListItemProps> =
|
||||
<Ionicons
|
||||
name={action.icon}
|
||||
size={18}
|
||||
color={action.variant === "destructive" ? "#800020" : "#374151"}
|
||||
color={
|
||||
action.variant === "destructive"
|
||||
? colors.iconDestructive
|
||||
: colors.iconDefault
|
||||
}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<UIText className={action.variant === "destructive" ? "text-destructive" : ""}>
|
||||
<UIText
|
||||
style={{
|
||||
color:
|
||||
action.variant === "destructive"
|
||||
? colors.iconDestructive
|
||||
: isDark
|
||||
? "#FFFFFF"
|
||||
: "#374151",
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</UIText>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
@@ -50,6 +51,7 @@ import {
|
||||
searchBookings,
|
||||
} from "@/utils/bookings-utils";
|
||||
import { offlineAwareRefresh } from "@/utils/network";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface BookingListScreenProps {
|
||||
// Platform-specific header rendering
|
||||
@@ -99,6 +101,9 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
const router = useRouter();
|
||||
const { userInfo } = useAuth();
|
||||
const [isManualRefreshing, setIsManualRefreshing] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
// Use React Query hook for fetching bookings
|
||||
const {
|
||||
@@ -598,8 +603,8 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
const renderListItem = ({ item }: { item: ListItem }) => {
|
||||
if (item.type === "monthHeader") {
|
||||
return (
|
||||
<View className="border-b border-[#E5E5EA] bg-[#f1f1f1] px-2 py-3 md:px-4">
|
||||
<Text className="text-base font-bold text-[#333]">{item.monthYear}</Text>
|
||||
<View className="border-b border-[#E5E5EA] bg-[#f1f1f1] px-2 py-3 dark:border-[#4D4D4D] dark:bg-[#171717] md:px-4">
|
||||
<Text className="text-base font-bold text-[#333] dark:text-white">{item.monthYear}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -633,7 +638,7 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
<View className="flex-1 bg-gray-50 dark:bg-black">
|
||||
{renderHeader?.()}
|
||||
{renderFilterControls?.()}
|
||||
<BookingListSkeleton count={4} iosStyle={iosStyle} />
|
||||
@@ -643,15 +648,17 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-50">
|
||||
<View className="flex-1 bg-gray-50 dark:bg-black">
|
||||
{renderHeader?.()}
|
||||
{renderFilterControls?.()}
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<Ionicons name="alert-circle" size={64} color="#800020" />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-gray-800">
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5 dark:bg-black">
|
||||
<Ionicons name="alert-circle" size={64} color={theme.destructive} />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-gray-800 dark:text-white">
|
||||
Unable to load bookings
|
||||
</Text>
|
||||
<Text className="mb-6 text-center text-base text-gray-500">{error}</Text>
|
||||
<Text className="mb-6 text-center text-base text-gray-500 dark:text-[#A3A3A3]">
|
||||
{error}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
@@ -671,7 +678,7 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
|
||||
{/* Empty state - no bookings */}
|
||||
<Activity mode={showEmptyState ? "visible" : "hidden"}>
|
||||
<View className="flex-1 bg-gray-50">
|
||||
<View className="flex-1 bg-gray-50 dark:bg-black">
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
padding: 20,
|
||||
@@ -680,7 +687,7 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
<RefreshControl refreshing={isManualRefreshing} onRefresh={manualRefresh} />
|
||||
}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: isDark ? "#000000" : "white" }}
|
||||
>
|
||||
<EmptyScreen
|
||||
icon={emptyState.icon}
|
||||
@@ -693,7 +700,7 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
|
||||
{/* Search empty state */}
|
||||
<Activity mode={showSearchEmptyState ? "visible" : "hidden"}>
|
||||
<View className="flex-1 bg-gray-50">
|
||||
<View className="flex-1 bg-gray-50 dark:bg-black">
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
padding: 20,
|
||||
@@ -702,7 +709,7 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
<RefreshControl refreshing={isManualRefreshing} onRefresh={manualRefresh} />
|
||||
}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: isDark ? "#000000" : "white" }}
|
||||
>
|
||||
<EmptyScreen
|
||||
icon="search-outline"
|
||||
@@ -728,13 +735,13 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
refreshControl={<RefreshControl refreshing={false} onRefresh={manualRefresh} />}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: isDark ? "#000000" : "white" }}
|
||||
/>
|
||||
</Activity>
|
||||
|
||||
<Activity mode={!iosStyle ? "visible" : "hidden"}>
|
||||
<View className="flex-1 px-2 pt-4 md:px-4">
|
||||
<View className="flex-1 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View className="flex-1 overflow-hidden rounded-lg border border-[#E5E5EA] bg-white dark:border-[#4D4D4D] dark:bg-black">
|
||||
<FlatList
|
||||
data={listItems}
|
||||
keyExtractor={(item) => item.key}
|
||||
@@ -790,31 +797,33 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
onRequestClose={handleCloseCancelModal}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-black/50 p-4">
|
||||
<View className="w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
||||
<View className="w-full max-w-md rounded-2xl bg-white shadow-2xl dark:bg-[#171717]">
|
||||
<View className="p-6">
|
||||
<View className="flex-row">
|
||||
{/* Danger icon */}
|
||||
<View className="mr-3 self-start rounded-full bg-red-50 p-2">
|
||||
<Ionicons name="alert-circle" size={20} color="#800000" />
|
||||
<View className="mr-3 self-start rounded-full bg-red-50 p-2 dark:bg-red-900/30">
|
||||
<Ionicons name="alert-circle" size={20} color={theme.destructive} />
|
||||
</View>
|
||||
|
||||
{/* Title and description */}
|
||||
<View className="flex-1">
|
||||
<Text className="mb-2 text-xl font-semibold text-gray-900">Cancel Event</Text>
|
||||
<Text className="text-sm leading-5 text-gray-600">
|
||||
<Text className="mb-2 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Cancel Event
|
||||
</Text>
|
||||
<Text className="text-sm leading-5 text-gray-600 dark:text-[#A3A3A3]">
|
||||
Are you sure you want to cancel "{cancelBooking?.title}"? Cancellation reason
|
||||
will be shared with guests.
|
||||
</Text>
|
||||
|
||||
{/* Reason Input */}
|
||||
<View className="mt-4">
|
||||
<Text className="mb-2 text-sm font-medium text-gray-700">
|
||||
<Text className="mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Reason for cancellation
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-base text-gray-900"
|
||||
className="rounded-lg border border-gray-300 bg-white px-3 py-2 text-base text-gray-900 dark:border-[#4D4D4D] dark:bg-[#2C2C2E] dark:text-white"
|
||||
placeholder="Why are you cancelling?"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={cancelReason}
|
||||
onChangeText={setCancelReason}
|
||||
multiline
|
||||
@@ -831,17 +840,21 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
<View className="flex-row-reverse gap-2 px-6 pb-6 pt-2">
|
||||
<TouchableOpacity
|
||||
className="rounded-lg px-4 py-2.5"
|
||||
style={{ backgroundColor: "#111827" }}
|
||||
style={{ backgroundColor: isDark ? "#FFFFFF" : "#111827" }}
|
||||
onPress={handleSubmitCancel}
|
||||
>
|
||||
<Text className="text-center text-base font-medium text-white">Cancel Event</Text>
|
||||
<Text className="text-center text-base font-medium text-white dark:text-black">
|
||||
Cancel Event
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2.5"
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2.5 dark:border-[#4D4D4D] dark:bg-[#2C2C2E]"
|
||||
onPress={handleCloseCancelModal}
|
||||
>
|
||||
<Text className="text-center text-base font-medium text-gray-700">Nevermind</Text>
|
||||
<Text className="text-center text-base font-medium text-gray-700 dark:text-white">
|
||||
Nevermind
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
@@ -855,7 +868,9 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader className="items-start">
|
||||
<AlertDialogTitle>
|
||||
<UIText className="text-left text-lg font-semibold">Cancel event</UIText>
|
||||
<UIText className="text-left text-lg font-semibold dark:text-white">
|
||||
Cancel event
|
||||
</UIText>
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
<UIText className="text-left text-sm text-muted-foreground">
|
||||
@@ -866,11 +881,13 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
|
||||
{/* Reason Input */}
|
||||
<View>
|
||||
<UIText className="mb-2 text-sm font-medium">Reason for cancellation</UIText>
|
||||
<UIText className="mb-2 text-sm font-medium dark:text-white">
|
||||
Reason for cancellation
|
||||
</UIText>
|
||||
<TextInput
|
||||
className="rounded-md border border-gray-300 bg-white px-3 py-2.5 text-base text-gray-900"
|
||||
className="rounded-md border border-gray-300 bg-white px-3 py-2.5 text-base text-gray-900 dark:border-[#4D4D4D] dark:bg-[#2C2C2E] dark:text-white"
|
||||
placeholder="Why are you cancelling?"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={cancelReason}
|
||||
onChangeText={setCancelReason}
|
||||
autoFocus
|
||||
@@ -906,12 +923,13 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
</AlertDialogHeader>
|
||||
|
||||
<View className="py-4">
|
||||
<Text className="mb-2 text-sm font-medium text-cal-text">
|
||||
<Text className="mb-2 text-sm font-medium text-cal-text dark:text-white">
|
||||
Cancellation Reason (required)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-lg border border-cal-border bg-cal-bg px-3 py-2 text-base text-cal-text"
|
||||
className="rounded-lg border border-cal-border bg-cal-bg px-3 py-2 text-base text-cal-text dark:border-[#4D4D4D] dark:bg-[#2C2C2E] dark:text-white"
|
||||
placeholder="Please provide a reason for cancelling"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={cancelAllReason}
|
||||
onChangeText={setCancelAllReason}
|
||||
multiline
|
||||
@@ -973,12 +991,13 @@ export const BookingListScreen: React.FC<BookingListScreenProps> = ({
|
||||
</AlertDialogHeader>
|
||||
|
||||
<View className="py-4">
|
||||
<Text className="mb-2 text-sm font-medium text-cal-text">
|
||||
<Text className="mb-2 text-sm font-medium text-cal-text dark:text-white">
|
||||
Rejection Reason (optional)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-lg border border-cal-border bg-cal-bg px-3 py-2 text-base text-cal-text"
|
||||
className="rounded-lg border border-cal-border bg-cal-bg px-3 py-2 text-base text-cal-text dark:border-[#4D4D4D] dark:bg-[#2C2C2E] dark:text-white"
|
||||
placeholder="Optionally provide a reason for rejecting"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={rejectAllReason}
|
||||
onChangeText={setRejectAllReason}
|
||||
multiline
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Alert, Platform, Switch, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
Switch,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { IOSPickerTrigger } from "./tabs/IOSPickerTrigger";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
// Section header
|
||||
export function SectionHeader({
|
||||
@@ -11,11 +20,13 @@ export function SectionHeader({
|
||||
title: string;
|
||||
rightElement?: React.ReactNode;
|
||||
}) {
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
return (
|
||||
<View className={`flex-row items-center ${rightElement ? "justify-between pr-4" : ""} mb-2`}>
|
||||
<Text
|
||||
className="ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72]"
|
||||
style={{ letterSpacing: 0.5 }}
|
||||
className="ml-4 text-[13px] uppercase tracking-wide"
|
||||
style={{ letterSpacing: 0.5, color: theme.textSecondary }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
@@ -36,11 +47,22 @@ export function SettingsGroup({
|
||||
headerRight?: React.ReactNode;
|
||||
footer?: string;
|
||||
}) {
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
return (
|
||||
<View>
|
||||
{header ? <SectionHeader title={header} rightElement={headerRight} /> : null}
|
||||
<View className="overflow-hidden rounded-[14px] bg-white">{children}</View>
|
||||
{footer ? <Text className="ml-4 mt-2 text-[13px] text-[#6D6D72]">{footer}</Text> : null}
|
||||
<View
|
||||
className="overflow-hidden rounded-[14px] bg-white"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
{footer ? (
|
||||
<Text className="ml-4 mt-2 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
{footer}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -63,6 +85,9 @@ export function SettingRow({
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
}) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const height = isFirst || isLast ? 52 : 44;
|
||||
const showDescription = () => {
|
||||
if (!description) return;
|
||||
@@ -84,10 +109,16 @@ export function SettingRow({
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View
|
||||
className={`flex-row items-center pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
|
||||
style={{ height, flexDirection: "row", alignItems: "center" }}
|
||||
className={`flex-row items-center pr-4`}
|
||||
style={{
|
||||
height,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
borderBottomWidth: !isLast ? 1 : 0,
|
||||
borderBottomColor: theme.borderSubtle,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="flex-1 flex-row items-center"
|
||||
@@ -96,7 +127,7 @@ export function SettingRow({
|
||||
activeOpacity={description ? 0.7 : 1}
|
||||
disabled={!description}
|
||||
>
|
||||
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
|
||||
<Text className="text-[17px]" style={{ fontWeight: "400", color: theme.text }}>
|
||||
{title}
|
||||
</Text>
|
||||
{description ? (
|
||||
@@ -107,8 +138,11 @@ export function SettingRow({
|
||||
<Switch
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
trackColor={{ false: "#E9E9EA", true: "#000000" }}
|
||||
thumbColor={Platform.OS !== "ios" ? "#FFFFFF" : undefined}
|
||||
trackColor={{
|
||||
false: isDark ? theme.backgroundEmphasis : "#E9E9EA",
|
||||
true: isDark ? "#34C759" : "#000000",
|
||||
}}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -135,22 +169,26 @@ export function NavigationRow({
|
||||
onSelect?: (value: string) => void;
|
||||
}) {
|
||||
const height = isFirst || isLast ? 52 : 44;
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
return (
|
||||
<View className="bg-white pl-4" style={{ height }}>
|
||||
<View className="bg-white pl-4" style={{ height, backgroundColor: theme.backgroundSecondary }}>
|
||||
<View
|
||||
className={`flex-1 flex-row items-center justify-between pr-4 ${
|
||||
!isLast ? "border-b border-[#E5E5E5]" : ""
|
||||
}`}
|
||||
style={{ height }}
|
||||
className={`flex-1 flex-row items-center justify-between pr-4`}
|
||||
style={{
|
||||
height,
|
||||
borderBottomWidth: !isLast ? 1 : 0,
|
||||
borderBottomColor: theme.borderSubtle,
|
||||
}}
|
||||
>
|
||||
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
|
||||
<Text className="text-[17px]" style={{ fontWeight: "400", color: theme.text }}>
|
||||
{title}
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
{Platform.OS === "ios" && options && onSelect ? (
|
||||
<>
|
||||
{value ? (
|
||||
<Text className="mr-2 text-[17px] text-[#8E8E93]" numberOfLines={1}>
|
||||
<Text className="mr-2 text-[17px] text-[#A3A3A3]" numberOfLines={1}>
|
||||
{value}
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -163,7 +201,7 @@ export function NavigationRow({
|
||||
activeOpacity={0.5}
|
||||
>
|
||||
{value ? (
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]" numberOfLines={1}>
|
||||
<Text className="mr-1 text-[17px] text-[#A3A3A3]" numberOfLines={1}>
|
||||
{value}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
import { showInfoAlert, showNotAvailableAlert } from "@/utils/alerts";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { NavigationRow, SettingRow, SettingsGroup } from "../SettingsUI";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { useColorScheme } from "react-native";
|
||||
|
||||
// Interface language options matching API V2 enum
|
||||
const interfaceLanguageOptions = [
|
||||
@@ -122,6 +124,8 @@ interface AdvancedTabProps {
|
||||
|
||||
export function AdvancedTab(props: AdvancedTabProps) {
|
||||
const [showLanguagePicker, setShowLanguagePicker] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
|
||||
const getLanguageLabel = (value: string) => {
|
||||
const option = interfaceLanguageOptions.find((opt) => opt.value === value);
|
||||
@@ -277,15 +281,21 @@ export function AdvancedTab(props: AdvancedTabProps) {
|
||||
{/* Seats Configuration - shown when enabled */}
|
||||
{props.seatsEnabled ? (
|
||||
<SettingsGroup header="Seats">
|
||||
<View className="bg-white pl-4">
|
||||
<View className="border-b border-[#E5E5E5] pt-4 pb-3 pr-4">
|
||||
<Text className="mb-2 text-[13px] text-[#6D6D72]">Seats per booking</Text>
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View
|
||||
className="pt-4 pb-3 pr-4"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: theme.borderSubtle }}
|
||||
>
|
||||
<Text className="mb-2 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
Seats per booking
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
|
||||
className="rounded-lg px-3 py-2 text-[17px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={props.seatsPerTimeSlot}
|
||||
onChangeText={props.setSeatsPerTimeSlot}
|
||||
placeholder="2"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
</View>
|
||||
@@ -336,12 +346,7 @@ export function AdvancedTab(props: AdvancedTabProps) {
|
||||
presentationStyle={Platform.OS === "ios" ? "formSheet" : undefined}
|
||||
onRequestClose={() => setShowLanguagePicker(false)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: Platform.OS === "ios" ? "#F2F2F7" : "rgba(0,0,0,0.5)",
|
||||
}}
|
||||
>
|
||||
<View className="flex-1 bg-[#F2F2F7] dark:bg-black">
|
||||
{Platform.OS !== "ios" ? (
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
@@ -350,27 +355,31 @@ export function AdvancedTab(props: AdvancedTabProps) {
|
||||
/>
|
||||
) : null}
|
||||
<View
|
||||
style={
|
||||
className={
|
||||
Platform.OS === "ios"
|
||||
? { flex: 1 }
|
||||
: {
|
||||
backgroundColor: "#F2F2F7",
|
||||
borderTopLeftRadius: 20,
|
||||
borderTopRightRadius: 20,
|
||||
height: "70%",
|
||||
width: "100%",
|
||||
}
|
||||
? "flex-1"
|
||||
: "h-[70%] w-full rounded-t-[20px] bg-[#F2F2F7] dark:bg-black"
|
||||
}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="h-[60px] flex-row items-center justify-between border-b border-[#E5E5E5] bg-white px-4">
|
||||
<View
|
||||
className="h-[60px] flex-row items-center justify-between px-4"
|
||||
style={{
|
||||
backgroundColor: theme.backgroundSecondary,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.borderSubtle,
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
onPress={() => setShowLanguagePicker(false)}
|
||||
className="h-8 w-8 items-center justify-center rounded-full bg-[#E5E5EA]"
|
||||
className="h-8 w-8 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: theme.backgroundEmphasis }}
|
||||
>
|
||||
<Ionicons name="close" size={20} color="#8E8E93" />
|
||||
<Ionicons name="close" size={20} color={theme.textMuted} />
|
||||
</TouchableOpacity>
|
||||
<Text className="text-[17px] font-semibold text-black">Select Language</Text>
|
||||
<Text className="text-[17px] font-semibold" style={{ color: theme.text }}>
|
||||
Select Language
|
||||
</Text>
|
||||
<View className="h-8 w-8" />
|
||||
</View>
|
||||
|
||||
@@ -378,23 +387,32 @@ export function AdvancedTab(props: AdvancedTabProps) {
|
||||
className="flex-1"
|
||||
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 40, paddingTop: 16 }}
|
||||
>
|
||||
<View className="overflow-hidden rounded-[10px] bg-white">
|
||||
<View
|
||||
className="overflow-hidden rounded-[10px]"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
{interfaceLanguageOptions.map((option, index) => (
|
||||
<View key={option.value} className="bg-white pl-4">
|
||||
<View
|
||||
key={option.value}
|
||||
className="pl-4"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center justify-between py-3 pr-4 ${
|
||||
index !== interfaceLanguageOptions.length - 1
|
||||
? "border-b border-[#E5E5E5]"
|
||||
: ""
|
||||
}`}
|
||||
className={`flex-row items-center justify-between py-3 pr-4`}
|
||||
style={{
|
||||
borderBottomWidth: index !== interfaceLanguageOptions.length - 1 ? 1 : 0,
|
||||
borderBottomColor: theme.borderSubtle,
|
||||
}}
|
||||
onPress={() => {
|
||||
props.setInterfaceLanguage(option.value);
|
||||
setShowLanguagePicker(false);
|
||||
}}
|
||||
>
|
||||
<Text className="text-[17px] text-black">{option.label}</Text>
|
||||
<Text className="text-[17px]" style={{ color: theme.text }}>
|
||||
{option.label}
|
||||
</Text>
|
||||
{props.interfaceLanguage === option.value ? (
|
||||
<Ionicons name="checkmark" size={20} color="#000000" />
|
||||
<Ionicons name="checkmark" size={20} color={theme.accent} />
|
||||
) : null}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
@@ -417,19 +435,25 @@ export function AdvancedTab(props: AdvancedTabProps) {
|
||||
/>
|
||||
{props.redirectEnabled ? (
|
||||
<>
|
||||
<View className="bg-white pl-4">
|
||||
<View className="border-b border-[#E5E5E5] pt-4 pb-3 pr-4">
|
||||
<Text className="mb-2 text-[13px] text-[#6D6D72]">Redirect URL</Text>
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View
|
||||
className="pt-4 pb-3 pr-4"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: theme.borderSubtle }}
|
||||
>
|
||||
<Text className="mb-2 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
Redirect URL
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
|
||||
className="rounded-lg px-3 py-2 text-[17px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={props.successRedirectUrl}
|
||||
onChangeText={props.setSuccessRedirectUrl}
|
||||
placeholder="https://example.com/thank-you"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
keyboardType="url"
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
<Text className="mt-2 text-[13px] text-[#FF9500]">
|
||||
<Text className="mt-2 text-[13px]" style={{ color: theme.warning }}>
|
||||
Adding a redirect will disable the success page.
|
||||
</Text>
|
||||
</View>
|
||||
@@ -471,49 +495,61 @@ export function AdvancedTab(props: AdvancedTabProps) {
|
||||
/>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* Event type colors */}
|
||||
{/* Event type colors */}
|
||||
<SettingsGroup header="Event Type Colors">
|
||||
<View className="bg-white pl-4">
|
||||
<View className="border-b border-[#E5E5E5] pt-4 pb-3 pr-4">
|
||||
<Text className="mb-2 text-[13px] text-[#6D6D72]">Light Theme</Text>
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View
|
||||
className="pt-4 pb-3 pr-4"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: theme.borderSubtle }}
|
||||
>
|
||||
<Text className="mb-2 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
Light Theme
|
||||
</Text>
|
||||
<View className="flex-row items-center gap-3">
|
||||
<View
|
||||
className="h-8 w-8 rounded-lg border border-[#C6C6C8]"
|
||||
className="h-8 w-8 rounded-lg border"
|
||||
style={{
|
||||
borderColor: theme.borderLight,
|
||||
backgroundColor: props.eventTypeColorLight.startsWith("#")
|
||||
? props.eventTypeColorLight
|
||||
: `#${props.eventTypeColorLight}`,
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
className="flex-1 rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
|
||||
className="flex-1 rounded-lg px-3 py-2 text-[17px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={props.eventTypeColorLight}
|
||||
onChangeText={props.setEventTypeColorLight}
|
||||
placeholder="292929"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View className="pt-3 pb-4 pr-4">
|
||||
<Text className="mb-2 text-[13px] text-[#6D6D72]">Dark Theme</Text>
|
||||
<Text className="mb-2 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
Dark Theme
|
||||
</Text>
|
||||
<View className="flex-row items-center gap-3">
|
||||
<View
|
||||
className="h-8 w-8 rounded-lg border border-[#C6C6C8]"
|
||||
className="h-8 w-8 rounded-lg border"
|
||||
style={{
|
||||
borderColor: theme.borderLight,
|
||||
backgroundColor: props.eventTypeColorDark.startsWith("#")
|
||||
? props.eventTypeColorDark
|
||||
: `#${props.eventTypeColorDark}`,
|
||||
}}
|
||||
/>
|
||||
<TextInput
|
||||
className="flex-1 rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
|
||||
className="flex-1 rounded-lg px-3 py-2 text-[17px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={props.eventTypeColorDark}
|
||||
onChangeText={props.setEventTypeColorDark}
|
||||
placeholder="fafafa"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
autoCapitalize="none"
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -52,7 +52,7 @@ const formatTime12Hour = (time24: string): string => {
|
||||
function SectionHeader({ title }: { title: string }) {
|
||||
return (
|
||||
<Text
|
||||
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72]"
|
||||
className="mb-2 ml-4 text-[13px] uppercase tracking-wide text-[#6D6D72] dark:text-[#A3A3A3]"
|
||||
style={{ letterSpacing: 0.5 }}
|
||||
>
|
||||
{title}
|
||||
@@ -65,7 +65,7 @@ function SettingsGroup({ children, header }: { children: React.ReactNode; header
|
||||
return (
|
||||
<View>
|
||||
{header ? <SectionHeader title={header} /> : null}
|
||||
<View className="overflow-hidden rounded-[14px] bg-white">{children}</View>
|
||||
<View className="overflow-hidden rounded-[14px] bg-white dark:bg-[#171717]">{children}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -89,21 +89,21 @@ function NavigationRow({
|
||||
onSelect?: (value: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<View className="bg-white pl-4" style={{ height: 44 }}>
|
||||
<View className="bg-white pl-4 dark:bg-[#171717]" style={{ height: 44 }}>
|
||||
<View
|
||||
className={`flex-1 flex-row items-center justify-between pr-4 ${
|
||||
!isLast ? "border-b border-[#E5E5E5]" : ""
|
||||
!isLast ? "border-b border-[#E5E5E5] dark:border-[#4D4D4D]" : ""
|
||||
}`}
|
||||
style={{ height: 44 }}
|
||||
>
|
||||
<Text className="text-[17px] text-black" style={{ fontWeight: "400" }}>
|
||||
<Text className="text-[17px] text-black dark:text-white" style={{ fontWeight: "400" }}>
|
||||
{title}
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
{Platform.OS === "ios" && options && onSelect ? (
|
||||
<>
|
||||
{value ? (
|
||||
<Text className="mr-2 text-[17px] text-[#8E8E93]" numberOfLines={1}>
|
||||
<Text className="mr-2 text-[17px] text-[#A3A3A3]" numberOfLines={1}>
|
||||
{value}
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -121,7 +121,7 @@ function NavigationRow({
|
||||
disabled={disabled}
|
||||
>
|
||||
{value ? (
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]" numberOfLines={1}>
|
||||
<Text className="mr-1 text-[17px] text-[#A3A3A3]" numberOfLines={1}>
|
||||
{value}
|
||||
</Text>
|
||||
) : null}
|
||||
@@ -164,13 +164,13 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
|
||||
{props.selectedSchedule ? (
|
||||
<SettingsGroup>
|
||||
{/* Header row */}
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4 dark:bg-[#171717]">
|
||||
<View
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4 dark:border-[#4D4D4D]"
|
||||
style={{ height: 44 }}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Weekly Schedule</Text>
|
||||
<Text className="text-[17px] text-[#8E8E93]">
|
||||
<Text className="text-[17px] text-black dark:text-white">Weekly Schedule</Text>
|
||||
<Text className="text-[17px] text-[#A3A3A3]">
|
||||
{enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -178,11 +178,11 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
|
||||
|
||||
{/* Day rows */}
|
||||
{props.scheduleDetailsLoading ? (
|
||||
<View className="items-center bg-white py-6">
|
||||
<Text className="text-[15px] italic text-[#8E8E93]">Loading schedule details...</Text>
|
||||
<View className="items-center bg-white py-6 dark:bg-[#171717]">
|
||||
<Text className="text-[15px] italic text-[#A3A3A3]">Loading schedule details...</Text>
|
||||
</View>
|
||||
) : props.selectedScheduleDetails ? (
|
||||
<View className="bg-white px-4 py-2">
|
||||
<View className="bg-white px-4 py-2 dark:bg-[#171717]">
|
||||
{DAYS.map((day, index) => {
|
||||
const dayInfo = daySchedules.find((d) => d.day === day);
|
||||
const isEnabled = dayInfo?.available ?? false;
|
||||
@@ -192,19 +192,19 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
|
||||
return (
|
||||
<View
|
||||
key={day}
|
||||
className={`flex-row items-center py-3 ${!isLast ? "border-b border-[#E5E5E5]" : ""}`}
|
||||
className={`flex-row items-center py-3 ${!isLast ? "border-b border-[#E5E5E5] dark:border-[#4D4D4D]" : ""}`}
|
||||
>
|
||||
{/* Availability indicator */}
|
||||
<View
|
||||
className={`mr-3 h-2.5 w-2.5 rounded-full ${
|
||||
isEnabled ? "bg-[#34C759]" : "bg-[#E5E5EA]"
|
||||
isEnabled ? "bg-[#34C759]" : "bg-[#E5E5EA] dark:bg-[#404040]"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Day name */}
|
||||
<Text
|
||||
className={`w-24 text-[15px] font-medium ${
|
||||
isEnabled ? "text-black" : "text-[#8E8E93]"
|
||||
isEnabled ? "text-black dark:text-white" : "text-[#A3A3A3]"
|
||||
}`}
|
||||
>
|
||||
{day}
|
||||
@@ -216,7 +216,7 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
|
||||
{timeSlots.map((slot, slotIndex) => (
|
||||
<Text
|
||||
key={`${slotIndex}-${slot.startTime}`}
|
||||
className={`text-[15px] text-black ${slotIndex > 0 ? "mt-1" : ""}`}
|
||||
className={`text-[15px] text-black dark:text-white ${slotIndex > 0 ? "mt-1" : ""}`}
|
||||
>
|
||||
{slot.startTime && slot.endTime
|
||||
? `${formatTime12Hour(slot.startTime)} - ${formatTime12Hour(slot.endTime)}`
|
||||
@@ -225,7 +225,7 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="flex-1 text-right text-[15px] text-[#8E8E93]">
|
||||
<Text className="flex-1 text-right text-[15px] text-[#A3A3A3]">
|
||||
Unavailable
|
||||
</Text>
|
||||
)}
|
||||
@@ -234,8 +234,8 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
|
||||
})}
|
||||
</View>
|
||||
) : (
|
||||
<View className="items-center bg-white py-6">
|
||||
<Text className="text-[15px] italic text-[#8E8E93]">
|
||||
<View className="items-center bg-white py-6 dark:bg-[#171717]">
|
||||
<Text className="text-[15px] italic text-[#A3A3A3]">
|
||||
Failed to load schedule details
|
||||
</Text>
|
||||
</View>
|
||||
@@ -246,8 +246,8 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
|
||||
{/* Timezone */}
|
||||
{props.selectedSchedule ? (
|
||||
<SettingsGroup header="Timezone">
|
||||
<View className="bg-white px-4 py-3">
|
||||
<Text className="text-center text-[17px] text-[#666]">
|
||||
<View className="bg-white px-4 py-3 dark:bg-[#171717]">
|
||||
<Text className="text-center text-[17px] text-[#666] dark:text-[#A3A3A3]">
|
||||
{props.selectedTimezone || props.selectedScheduleDetails?.timeZone || "No timezone"}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { LocationItem, LocationOptionGroup } from "@/types/locations";
|
||||
import { createLocationItemFromOption } from "@/utils/locationHelpers";
|
||||
import { slugify } from "@/utils/slugify";
|
||||
import { NavigationRow, SettingRow, SettingsGroup } from "../SettingsUI";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { useColorScheme } from "react-native";
|
||||
|
||||
interface BasicsTabProps {
|
||||
eventTitle: string;
|
||||
@@ -63,21 +65,31 @@ function InputRow({
|
||||
isFirst?: boolean;
|
||||
isLast?: boolean;
|
||||
}) {
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
return (
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View
|
||||
className={`pr-4 ${!isLast ? "border-b border-[#E5E5E5]" : ""} ${
|
||||
isFirst ? "pt-4 pb-3" : isLast ? "pt-3 pb-4" : "py-3"
|
||||
}`}
|
||||
className={`pr-4 ${isFirst ? "pt-4 pb-3" : isLast ? "pt-3 pb-4" : "py-3"}`}
|
||||
style={{
|
||||
borderBottomWidth: !isLast ? 1 : 0,
|
||||
borderBottomColor: theme.borderSubtle,
|
||||
}}
|
||||
>
|
||||
<Text className="mb-2 text-[13px] text-[#6D6D72]">{label}</Text>
|
||||
<Text className="mb-2 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
{label}
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[17px] text-black"
|
||||
style={multiline ? { height: 80, textAlignVertical: "top" } : undefined}
|
||||
className="rounded-lg px-3 py-2 text-[17px]"
|
||||
style={{
|
||||
backgroundColor: theme.backgroundMuted,
|
||||
color: theme.text,
|
||||
...(multiline ? { height: 80, textAlignVertical: "top" } : undefined),
|
||||
}}
|
||||
value={value}
|
||||
onChangeText={onChangeText}
|
||||
placeholder={placeholder}
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor="#A3A3A3"
|
||||
multiline={multiline}
|
||||
numberOfLines={numberOfLines}
|
||||
keyboardType={keyboardType}
|
||||
@@ -91,6 +103,8 @@ function InputRow({
|
||||
|
||||
export const BasicsTab: React.FC<BasicsTabProps> = (props) => {
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
|
||||
const onSelectLocation = (value: string, label: string) => {
|
||||
const newLocation = createLocationItemFromOption(value, label);
|
||||
@@ -118,11 +132,19 @@ export const BasicsTab: React.FC<BasicsTabProps> = (props) => {
|
||||
numberOfLines={4}
|
||||
/>
|
||||
{/* URL Input */}
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View className="pr-4 pt-3 pb-4">
|
||||
<Text className="mb-2 text-[13px] text-[#6D6D72]">URL</Text>
|
||||
<View className="flex-row items-center overflow-hidden rounded-lg bg-[#F2F2F7]">
|
||||
<Text className="bg-[#E5E5EA] px-3 py-2 text-[15px] text-[#666]">
|
||||
<Text className="mb-2 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
URL
|
||||
</Text>
|
||||
<View
|
||||
className="flex-row items-center overflow-hidden rounded-lg"
|
||||
style={{ backgroundColor: theme.backgroundMuted }}
|
||||
>
|
||||
<Text
|
||||
className="px-3 py-2 text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundEmphasis, color: theme.textSecondary }}
|
||||
>
|
||||
{(() => {
|
||||
// Parse bookingUrl to get domain prefix (e.g., "i.cal.com/" or "cal.com/username/")
|
||||
if (props.bookingUrl) {
|
||||
@@ -145,11 +167,12 @@ export const BasicsTab: React.FC<BasicsTabProps> = (props) => {
|
||||
})()}
|
||||
</Text>
|
||||
<TextInput
|
||||
className="flex-1 px-3 py-2 text-[17px] text-black"
|
||||
className="flex-1 px-3 py-2 text-[17px]"
|
||||
style={{ color: theme.text }}
|
||||
value={props.eventSlug}
|
||||
onChangeText={(text) => props.setEventSlug(slugify(text, true))}
|
||||
placeholder="event-slug"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor="#A3A3A3"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -159,22 +182,30 @@ export const BasicsTab: React.FC<BasicsTabProps> = (props) => {
|
||||
{/* Duration */}
|
||||
<SettingsGroup header="Duration">
|
||||
{!props.allowMultipleDurations ? (
|
||||
<View className="bg-white pl-4" style={{ height: 52 }}>
|
||||
<View
|
||||
className="bg-white pl-4"
|
||||
style={{ height: 52, backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<View
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
|
||||
style={{ height: 52 }}
|
||||
className="flex-row items-center justify-between pr-4"
|
||||
style={{ height: 52, borderBottomWidth: 1, borderBottomColor: theme.borderSubtle }}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Duration</Text>
|
||||
<Text className="text-[17px]" style={{ color: theme.text }}>
|
||||
Duration
|
||||
</Text>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<TextInput
|
||||
className="w-16 rounded-lg bg-[#F2F2F7] px-2 py-1.5 text-center text-[15px] text-black"
|
||||
className="w-16 rounded-lg px-2 py-1.5 text-center text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={props.eventDuration}
|
||||
onChangeText={props.setEventDuration}
|
||||
placeholder="30"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor="#A3A3A3"
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<Text className="text-[15px] text-[#6D6D72]">minutes</Text>
|
||||
<Text className="text-[15px]" style={{ color: theme.textSecondary }}>
|
||||
minutes
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -11,6 +11,8 @@ import { type Animated, Platform, Text, TextInput, TouchableOpacity, View } from
|
||||
import { NavigationRow, SettingRow, SettingsGroup } from "../SettingsUI";
|
||||
import { LimitsTabDatePicker } from "./LimitsTabDatePicker";
|
||||
import { LimitsTabIOSPicker } from "./LimitsTabIOSPicker";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { useColorScheme } from "react-native";
|
||||
|
||||
interface FrequencyLimit {
|
||||
id: number;
|
||||
@@ -65,15 +67,21 @@ const DURATION_UNIT_OPTIONS = ["Per day", "Per week", "Per month"];
|
||||
|
||||
// Local components removed in favor of SettingsUI
|
||||
|
||||
// Android/Web picker button (opens modal)
|
||||
// Android/Web picker button (opens modal)
|
||||
function ModalPickerButton({ value, onPress }: { value: string; onPress: () => void }) {
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center rounded-lg bg-[#F2F2F7] px-2 py-1.5"
|
||||
className="flex-row items-center rounded-lg px-2 py-1.5"
|
||||
style={{ backgroundColor: theme.backgroundMuted }}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text className="text-[15px] text-black">{value}</Text>
|
||||
<Ionicons name="chevron-down" size={14} color="#8E8E93" style={{ marginLeft: 4 }} />
|
||||
<Text className="text-[15px]" style={{ color: theme.text }}>
|
||||
{value}
|
||||
</Text>
|
||||
<Ionicons name="chevron-down" size={14} color={theme.textMuted} style={{ marginLeft: 4 }} />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@@ -134,6 +142,9 @@ interface LimitsTabProps {
|
||||
}
|
||||
|
||||
export function LimitsTab(props: LimitsTabProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
|
||||
return (
|
||||
<View className="gap-6">
|
||||
{/* Buffer & Notice Settings */}
|
||||
@@ -152,27 +163,32 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
onSelect={props.setAfterEventBuffer}
|
||||
onPress={() => props.setShowAfterBufferDropdown(true)}
|
||||
/>
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
|
||||
style={{ height: 44 }}
|
||||
className="flex-row items-center justify-between pr-4"
|
||||
style={{ height: 44, borderBottomWidth: 1, borderBottomColor: theme.borderSubtle }}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Minimum Notice</Text>
|
||||
<Text className="text-[17px]" style={{ color: theme.text }}>
|
||||
Minimum Notice
|
||||
</Text>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<TextInput
|
||||
className="w-16 rounded-lg bg-[#F2F2F7] px-2 py-1.5 text-center text-[15px] text-black"
|
||||
className="w-16 rounded-lg px-2 py-1.5 text-center text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={props.minimumNoticeValue}
|
||||
onChangeText={(text) => {
|
||||
const numericValue = text.replace(/[^0-9]/g, "");
|
||||
props.setMinimumNoticeValue(numericValue);
|
||||
}}
|
||||
placeholder="1"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
{Platform.OS === "ios" ? (
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">{props.minimumNoticeUnit}</Text>
|
||||
<Text className="mr-1 text-[17px]" style={{ color: theme.textMuted }}>
|
||||
{props.minimumNoticeUnit}
|
||||
</Text>
|
||||
<LimitsTabIOSPicker
|
||||
options={TIME_UNIT_OPTIONS}
|
||||
selectedValue={props.minimumNoticeUnit}
|
||||
@@ -237,28 +253,39 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
{props.limitBookingFrequency ? (
|
||||
<SettingsGroup header="Frequency Limits">
|
||||
{props.frequencyLimits.map((limit, index) => (
|
||||
<View key={limit.id} className="bg-white pl-4">
|
||||
<View
|
||||
key={limit.id}
|
||||
className="bg-white pl-4"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<View
|
||||
className={`flex-row items-center justify-between pr-4 py-3 ${
|
||||
index !== props.frequencyLimits.length - 1 ? "border-b border-[#E5E5E5]" : ""
|
||||
}`}
|
||||
className={`flex-row items-center justify-between pr-4 py-3`}
|
||||
style={{
|
||||
borderBottomWidth: index !== props.frequencyLimits.length - 1 ? 1 : 0,
|
||||
borderBottomColor: theme.borderSubtle,
|
||||
}}
|
||||
>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<TextInput
|
||||
className="w-16 rounded-lg bg-[#F2F2F7] px-2 py-1.5 text-center text-[15px] text-black"
|
||||
className="w-16 rounded-lg px-2 py-1.5 text-center text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={limit.value}
|
||||
onChangeText={(text) => {
|
||||
const numericValue = text.replace(/[^0-9]/g, "");
|
||||
props.updateFrequencyLimit(limit.id, "value", numericValue);
|
||||
}}
|
||||
placeholder="1"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<Text className="text-[15px] text-[#6D6D72]">per</Text>
|
||||
<Text className="text-[15px]" style={{ color: theme.textSecondary }}>
|
||||
per
|
||||
</Text>
|
||||
{Platform.OS === "ios" ? (
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">{limit.unit}</Text>
|
||||
<Text className="mr-1 text-[17px]" style={{ color: theme.textMuted }}>
|
||||
{limit.unit}
|
||||
</Text>
|
||||
<LimitsTabIOSPicker
|
||||
options={FREQUENCY_UNIT_OPTIONS}
|
||||
selectedValue={limit.unit}
|
||||
@@ -276,18 +303,21 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
</View>
|
||||
{props.frequencyLimits.length > 1 ? (
|
||||
<TouchableOpacity onPress={() => props.removeFrequencyLimit(limit.id)}>
|
||||
<Ionicons name="trash-outline" size={20} color="#FF3B30" />
|
||||
<Ionicons name="trash-outline" size={20} color={theme.destructive} />
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center bg-white py-3"
|
||||
className="flex-row items-center justify-center py-3"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
onPress={props.addFrequencyLimit}
|
||||
>
|
||||
<Ionicons name="add" size={20} color="#007AFF" />
|
||||
<Text className="ml-1 text-[17px] text-[#007AFF]">Add Limit</Text>
|
||||
<Ionicons name="add" size={20} color={theme.accent} />
|
||||
<Text className="ml-1 text-[17px]" style={{ color: theme.accent }}>
|
||||
Add Limit
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</SettingsGroup>
|
||||
) : null}
|
||||
@@ -296,28 +326,39 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
{props.limitTotalDuration ? (
|
||||
<SettingsGroup header="Duration Limits">
|
||||
{props.durationLimits.map((limit, index) => (
|
||||
<View key={limit.id} className="bg-white pl-4">
|
||||
<View
|
||||
key={limit.id}
|
||||
className="bg-white pl-4"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
>
|
||||
<View
|
||||
className={`flex-row items-center justify-between pr-4 py-3 ${
|
||||
index !== props.durationLimits.length - 1 ? "border-b border-[#E5E5E5]" : ""
|
||||
}`}
|
||||
className={`flex-row items-center justify-between pr-4 py-3`}
|
||||
style={{
|
||||
borderBottomWidth: index !== props.durationLimits.length - 1 ? 1 : 0,
|
||||
borderBottomColor: theme.borderSubtle,
|
||||
}}
|
||||
>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<TextInput
|
||||
className="w-16 rounded-lg bg-[#F2F2F7] px-2 py-1.5 text-center text-[15px] text-black"
|
||||
className="w-16 rounded-lg px-2 py-1.5 text-center text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={limit.value}
|
||||
onChangeText={(text) => {
|
||||
const numericValue = text.replace(/[^0-9]/g, "");
|
||||
props.updateDurationLimit(limit.id, "value", numericValue);
|
||||
}}
|
||||
placeholder="60"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<Text className="text-[15px] text-[#6D6D72]">minutes per</Text>
|
||||
<Text className="text-[15px]" style={{ color: theme.textSecondary }}>
|
||||
minutes per
|
||||
</Text>
|
||||
{Platform.OS === "ios" ? (
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">{limit.unit}</Text>
|
||||
<Text className="mr-1 text-[17px]" style={{ color: theme.textMuted }}>
|
||||
{limit.unit}
|
||||
</Text>
|
||||
<LimitsTabIOSPicker
|
||||
options={DURATION_UNIT_OPTIONS}
|
||||
selectedValue={limit.unit}
|
||||
@@ -335,18 +376,21 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
</View>
|
||||
{props.durationLimits.length > 1 ? (
|
||||
<TouchableOpacity onPress={() => props.removeDurationLimit(limit.id)}>
|
||||
<Ionicons name="trash-outline" size={20} color="#FF3B30" />
|
||||
<Ionicons name="trash-outline" size={20} color={theme.destructive} />
|
||||
</TouchableOpacity>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center bg-white py-3"
|
||||
className="flex-row items-center justify-center py-3"
|
||||
style={{ backgroundColor: theme.background }}
|
||||
onPress={props.addDurationLimit}
|
||||
>
|
||||
<Ionicons name="add" size={20} color="#007AFF" />
|
||||
<Text className="ml-1 text-[17px] text-[#007AFF]">Add Limit</Text>
|
||||
<Ionicons name="add" size={20} color={theme.accent} />
|
||||
<Text className="ml-1 text-[17px]" style={{ color: theme.accent }}>
|
||||
Add Limit
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</SettingsGroup>
|
||||
) : null}
|
||||
@@ -354,25 +398,30 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
{/* Max Active Bookings Configuration */}
|
||||
{props.maxActiveBookingsPerBooker ? (
|
||||
<SettingsGroup header="Active Bookings Limit">
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
|
||||
style={{ height: 44 }}
|
||||
className="flex-row items-center justify-between pr-4"
|
||||
style={{ height: 44, borderBottomWidth: 1, borderBottomColor: theme.borderSubtle }}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Max bookings</Text>
|
||||
<Text className="text-[17px]" style={{ color: theme.text }}>
|
||||
Max bookings
|
||||
</Text>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<TextInput
|
||||
className="w-16 rounded-lg bg-[#F2F2F7] px-2 py-1.5 text-center text-[15px] text-black"
|
||||
className="w-16 rounded-lg px-2 py-1.5 text-center text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={props.maxActiveBookingsValue}
|
||||
onChangeText={(text) => {
|
||||
const numericValue = text.replace(/[^0-9]/g, "");
|
||||
props.setMaxActiveBookingsValue(numericValue);
|
||||
}}
|
||||
placeholder="1"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<Text className="text-[15px] text-[#6D6D72]">bookings</Text>
|
||||
<Text className="text-[15px]" style={{ color: theme.textSecondary }}>
|
||||
bookings
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
@@ -392,22 +441,32 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
{/* Rolling option */}
|
||||
<TouchableOpacity
|
||||
className="bg-white pl-4"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
onPress={() => props.setFutureBookingType("rolling")}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View className="flex-row items-center border-b border-[#E5E5E5] pr-4 py-3">
|
||||
<View
|
||||
className="flex-row items-center pr-4 py-3"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: theme.borderSubtle }}
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-5 w-5 items-center justify-center rounded-full border-2 ${
|
||||
props.futureBookingType === "rolling" ? "border-[#007AFF]" : "border-[#C7C7CC]"
|
||||
}`}
|
||||
className={`mr-3 h-5 w-5 items-center justify-center rounded-full border-2`}
|
||||
style={{
|
||||
borderColor:
|
||||
props.futureBookingType === "rolling" ? theme.accent : theme.borderLight,
|
||||
}}
|
||||
>
|
||||
{props.futureBookingType === "rolling" ? (
|
||||
<View className="h-2.5 w-2.5 rounded-full bg-[#007AFF]" />
|
||||
<View
|
||||
className="h-2.5 w-2.5 rounded-full"
|
||||
style={{ backgroundColor: theme.accent }}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<View className="flex-1 flex-row flex-wrap items-center gap-2">
|
||||
<TextInput
|
||||
className="w-14 rounded-lg bg-[#F2F2F7] px-2 py-1.5 text-center text-[15px] text-black"
|
||||
className="w-14 rounded-lg px-2 py-1.5 text-center text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={props.rollingDays}
|
||||
onChangeText={(text) => {
|
||||
const numericValue = text.replace(/[^0-9]/g, "");
|
||||
@@ -415,21 +474,24 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
props.setFutureBookingType("rolling");
|
||||
}}
|
||||
placeholder="30"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="rounded-lg bg-[#F2F2F7] px-2 py-1.5"
|
||||
className="rounded-lg px-2 py-1.5"
|
||||
style={{ backgroundColor: theme.backgroundMuted }}
|
||||
onPress={() => {
|
||||
props.setRollingCalendarDays(!props.rollingCalendarDays);
|
||||
props.setFutureBookingType("rolling");
|
||||
}}
|
||||
>
|
||||
<Text className="text-[15px] text-black">
|
||||
<Text className="text-[15px]" style={{ color: theme.text }}>
|
||||
{props.rollingCalendarDays ? "calendar" : "business"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<Text className="text-[15px] text-[#6D6D72]">days ahead</Text>
|
||||
<Text className="text-[15px]" style={{ color: theme.textSecondary }}>
|
||||
days ahead
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -437,25 +499,35 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
{/* Date Range option */}
|
||||
<TouchableOpacity
|
||||
className="bg-white pl-4"
|
||||
style={{ backgroundColor: theme.backgroundSecondary }}
|
||||
onPress={() => props.setFutureBookingType("range")}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View className="flex-row items-start pr-4 py-3">
|
||||
<View
|
||||
className={`mr-3 mt-0.5 h-5 w-5 items-center justify-center rounded-full border-2 ${
|
||||
props.futureBookingType === "range" ? "border-[#007AFF]" : "border-[#C7C7CC]"
|
||||
}`}
|
||||
className={`mr-3 mt-0.5 h-5 w-5 items-center justify-center rounded-full border-2`}
|
||||
style={{
|
||||
borderColor:
|
||||
props.futureBookingType === "range" ? theme.accent : theme.borderLight,
|
||||
}}
|
||||
>
|
||||
{props.futureBookingType === "range" ? (
|
||||
<View className="h-2.5 w-2.5 rounded-full bg-[#007AFF]" />
|
||||
<View
|
||||
className="h-2.5 w-2.5 rounded-full"
|
||||
style={{ backgroundColor: theme.accent }}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="mb-2 text-[17px] text-black">Within a date range</Text>
|
||||
<Text className="mb-2 text-[17px]" style={{ color: theme.text }}>
|
||||
Within a date range
|
||||
</Text>
|
||||
{props.futureBookingType === "range" ? (
|
||||
<View className="gap-3">
|
||||
<View className="flex-row items-center">
|
||||
<Text className="w-20 text-[13px] text-[#6D6D72]">Start date</Text>
|
||||
<Text className="w-20 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
Start date
|
||||
</Text>
|
||||
<LimitsTabDatePicker
|
||||
value={props.rangeStartDate}
|
||||
onChange={props.setRangeStartDate}
|
||||
@@ -463,7 +535,9 @@ export function LimitsTab(props: LimitsTabProps) {
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="w-20 text-[13px] text-[#6D6D72]">End date</Text>
|
||||
<Text className="w-20 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
End date
|
||||
</Text>
|
||||
<LimitsTabDatePicker
|
||||
value={props.rangeEndDate}
|
||||
onChange={props.setRangeEndDate}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { Pressable, Text } from "react-native";
|
||||
import { Pressable, Text, useColorScheme } from "react-native";
|
||||
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface LimitsTabDatePickerProps {
|
||||
value: string;
|
||||
@@ -50,9 +51,17 @@ export function LimitsTabDatePicker({ value, onChange, placeholder }: LimitsTabD
|
||||
});
|
||||
}, [dateValue, handleDateChange]);
|
||||
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
return (
|
||||
<Pressable onPress={openDatePicker} className="rounded-lg bg-[#F2F2F7] px-3 py-2">
|
||||
<Text className={`text-[15px] ${value ? "text-black" : "text-[#8E8E93]"}`}>
|
||||
<Pressable
|
||||
onPress={openDatePicker}
|
||||
className="rounded-lg px-3 py-2"
|
||||
style={{ backgroundColor: theme.backgroundMuted }}
|
||||
>
|
||||
<Text className="text-[15px]" style={{ color: value ? theme.text : theme.textMuted }}>
|
||||
{displayText}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TextInput, View } from "react-native";
|
||||
import { TextInput, View, useColorScheme } from "react-native";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface LimitsTabDatePickerProps {
|
||||
value: string;
|
||||
@@ -7,14 +8,19 @@ interface LimitsTabDatePickerProps {
|
||||
}
|
||||
|
||||
export function LimitsTabDatePicker({ value, onChange, placeholder }: LimitsTabDatePickerProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<TextInput
|
||||
className="rounded-lg bg-[#F2F2F7] px-3 py-2 text-[15px] text-black"
|
||||
className="rounded-lg px-3 py-2 text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={value}
|
||||
onChangeText={onChange}
|
||||
placeholder={placeholder || "YYYY-MM-DD"}
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor="#A3A3A3"
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
* iOS Settings style with grouped rows and section headers.
|
||||
*/
|
||||
|
||||
import { Platform, Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { Platform, Text, TextInput, TouchableOpacity, View, useColorScheme } from "react-native";
|
||||
import { SettingRow, SettingsGroup } from "../SettingsUI";
|
||||
import { RecurringTabIOSPicker } from "./RecurringTabIOSPicker";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface RecurringTabProps {
|
||||
recurringEnabled: boolean;
|
||||
@@ -41,6 +42,9 @@ export function RecurringTab({
|
||||
setRecurringOccurrences,
|
||||
setShowFrequencyDropdown,
|
||||
}: RecurringTabProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const theme = getColors(colorScheme === "dark");
|
||||
|
||||
return (
|
||||
<View className="gap-6">
|
||||
{/* Recurring Toggle */}
|
||||
@@ -59,15 +63,22 @@ export function RecurringTab({
|
||||
{recurringEnabled ? (
|
||||
<SettingsGroup header="Recurrence">
|
||||
{/* Repeats Every */}
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View
|
||||
className="flex-row items-center justify-between border-b border-[#E5E5E5] pr-4"
|
||||
style={{ height: 44 }}
|
||||
className="flex-row items-center justify-between pr-4"
|
||||
style={{
|
||||
height: 44,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.borderSubtle,
|
||||
}}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Repeats every</Text>
|
||||
<Text className="text-[17px]" style={{ color: theme.text }}>
|
||||
Repeats every
|
||||
</Text>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<TextInput
|
||||
className="w-14 rounded-lg bg-[#F2F2F7] px-2 py-1.5 text-center text-[15px] text-black"
|
||||
className="w-14 rounded-lg px-2 py-1.5 text-center text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={recurringInterval}
|
||||
onChangeText={(text) => {
|
||||
const numericValue = text.replace(/[^0-9]/g, "");
|
||||
@@ -81,12 +92,12 @@ export function RecurringTab({
|
||||
}
|
||||
}}
|
||||
placeholder="1"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
{Platform.OS === "ios" ? (
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">
|
||||
<Text className="mr-1 text-[17px]" style={{ color: theme.textMuted }}>
|
||||
{frequencyToLabel[recurringFrequency] || recurringFrequency}
|
||||
</Text>
|
||||
<RecurringTabIOSPicker
|
||||
@@ -103,16 +114,17 @@ export function RecurringTab({
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center rounded-lg bg-[#F2F2F7] px-2 py-1.5"
|
||||
className="flex-row items-center rounded-lg px-2 py-1.5"
|
||||
style={{ backgroundColor: theme.backgroundMuted }}
|
||||
onPress={() => setShowFrequencyDropdown(true)}
|
||||
>
|
||||
<Text className="text-[15px] text-black">
|
||||
<Text className="text-[15px]" style={{ color: theme.text }}>
|
||||
{frequencyToLabel[recurringFrequency] || recurringFrequency}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name="chevron-down"
|
||||
size={14}
|
||||
color="#8E8E93"
|
||||
color={theme.textMuted}
|
||||
style={{ marginLeft: 4 }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
@@ -122,12 +134,15 @@ export function RecurringTab({
|
||||
</View>
|
||||
|
||||
{/* Maximum occurrences */}
|
||||
<View className="bg-white pl-4">
|
||||
<View className="bg-white pl-4" style={{ backgroundColor: theme.backgroundSecondary }}>
|
||||
<View className="flex-row items-center justify-between pr-4" style={{ height: 44 }}>
|
||||
<Text className="text-[17px] text-black">Maximum events</Text>
|
||||
<Text className="text-[17px]" style={{ color: theme.text }}>
|
||||
Maximum events
|
||||
</Text>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<TextInput
|
||||
className="w-14 rounded-lg bg-[#F2F2F7] px-2 py-1.5 text-center text-[15px] text-black"
|
||||
className="w-14 rounded-lg px-2 py-1.5 text-center text-[15px]"
|
||||
style={{ backgroundColor: theme.backgroundMuted, color: theme.text }}
|
||||
value={recurringOccurrences}
|
||||
onChangeText={(text) => {
|
||||
const numericValue = text.replace(/[^0-9]/g, "");
|
||||
@@ -141,10 +156,12 @@ export function RecurringTab({
|
||||
}
|
||||
}}
|
||||
placeholder="12"
|
||||
placeholderTextColor="#8E8E93"
|
||||
placeholderTextColor={theme.textMuted}
|
||||
keyboardType="numeric"
|
||||
/>
|
||||
<Text className="text-[15px] text-[#6D6D72]">events</Text>
|
||||
<Text className="text-[15px]" style={{ color: theme.textSecondary }}>
|
||||
events
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
|
||||
import { buttonStyle, frame } from "@expo/ui/swift-ui/modifiers";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import type React from "react";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { Pressable, useColorScheme, View } from "react-native";
|
||||
import { EventTypeBadges, EventTypeDescription, EventTypeTitle } from "./EventTypeListItemParts";
|
||||
import type { EventTypeListItemProps } from "./types";
|
||||
import { useEventTypeListItemData } from "./useEventTypeListItemData";
|
||||
@@ -20,6 +20,8 @@ export const EventTypeListItem = ({
|
||||
const { formattedDuration, normalizedDescription, hasPrice, formattedPrice } =
|
||||
useEventTypeListItemData(item);
|
||||
const isLast = index === filteredEventTypes.length - 1;
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
// Calculate badge count to force remount when badges change
|
||||
// This fixes SwiftUI Host caching stale layout measurements
|
||||
@@ -88,7 +90,11 @@ export const EventTypeListItem = ({
|
||||
// This fixes SwiftUI Host caching stale layout measurements
|
||||
return (
|
||||
<View
|
||||
className={`bg-cal-bg active:bg-cal-bg-secondary ${!isLast ? "border-b border-cal-border" : ""}`}
|
||||
style={{
|
||||
backgroundColor: isDark ? "#000000" : "#FFFFFF",
|
||||
borderBottomWidth: !isLast ? 1 : 0,
|
||||
borderBottomColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
}}
|
||||
>
|
||||
{/* Calculate minHeight based on badge rows to ensure proper spacing */}
|
||||
{/* 1-3 badges = 1 row, 4-5 badges = likely 2 rows, 6 badges = 2 rows */}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Pressable, View } from "react-native";
|
||||
import { getColors } from "@/constants/colors";
|
||||
import { Pressable, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -28,6 +29,14 @@ export const EventTypeListItem = ({
|
||||
useEventTypeListItemData(item);
|
||||
const isLast = index === filteredEventTypes.length - 1;
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const colors = {
|
||||
icon: isDark ? "#FFFFFF" : "#3C3F44",
|
||||
iconMenu: isDark ? "#E5E5EA" : "#374151",
|
||||
};
|
||||
|
||||
const contentInsets = {
|
||||
top: insets.top,
|
||||
@@ -37,7 +46,13 @@ export const EventTypeListItem = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<View className={`bg-cal-bg ${!isLast ? "border-b border-cal-border" : ""}`}>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isDark ? "#000000" : "#FFFFFF",
|
||||
borderBottomWidth: !isLast ? 1 : 0,
|
||||
borderBottomColor: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
||||
className="flex-row items-center justify-between"
|
||||
@@ -45,7 +60,7 @@ export const EventTypeListItem = ({
|
||||
<Pressable
|
||||
onPress={() => handleEventTypePress(item)}
|
||||
style={{ flex: 1, marginRight: 12 }}
|
||||
android_ripple={{ color: "rgba(0, 0, 0, 0.1)" }}
|
||||
android_ripple={{ color: isDark ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.1)" }}
|
||||
>
|
||||
<EventTypeTitle
|
||||
title={item.title}
|
||||
@@ -68,21 +83,31 @@ export const EventTypeListItem = ({
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Pressable
|
||||
className="items-center justify-center rounded-lg border border-cal-border"
|
||||
className="items-center justify-center rounded-lg border border-cal-border bg-white dark:border-cal-border-dark dark:bg-[#171717]"
|
||||
style={{ width: 36, height: 36, flexShrink: 0 }}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color={colors.icon} />
|
||||
</Pressable>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent insets={contentInsets} sideOffset={8} className="w-44" align="end">
|
||||
<DropdownMenuItem onPress={() => handlePreview(item)}>
|
||||
<Ionicons name="open-outline" size={18} color="#374151" style={{ marginRight: 8 }} />
|
||||
<Ionicons
|
||||
name="open-outline"
|
||||
size={18}
|
||||
color={colors.iconMenu}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text>Preview</Text>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onPress={() => handleCopyLink(item)}>
|
||||
<Ionicons name="link-outline" size={18} color="#374151" style={{ marginRight: 8 }} />
|
||||
<Ionicons
|
||||
name="link-outline"
|
||||
size={18}
|
||||
color={colors.iconMenu}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text>Copy link</Text>
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -92,22 +117,32 @@ export const EventTypeListItem = ({
|
||||
<Ionicons
|
||||
name="pencil-outline"
|
||||
size={18}
|
||||
color="#374151"
|
||||
color={colors.iconMenu}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text>Edit</Text>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuItem onPress={() => onDuplicate?.(item)}>
|
||||
<Ionicons name="copy-outline" size={18} color="#374151" style={{ marginRight: 8 }} />
|
||||
<Ionicons
|
||||
name="copy-outline"
|
||||
size={18}
|
||||
color={colors.iconMenu}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text>Duplicate</Text>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem variant="destructive" onPress={() => onDelete?.(item)}>
|
||||
<Ionicons name="trash-outline" size={18} color="#800020" style={{ marginRight: 8 }} />
|
||||
<Text className="text-destructive">Delete</Text>
|
||||
<Ionicons
|
||||
name="trash-outline"
|
||||
size={18}
|
||||
color={theme.destructive}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text style={{ color: theme.destructive }}>Delete</Text>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import { Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
import { Tooltip } from "@/components/Tooltip";
|
||||
import type { EventType } from "@/services/types/event-types.types";
|
||||
@@ -32,8 +32,8 @@ export function EventTypeTitle({ title, username, slug, bookingUrl }: EventTypeT
|
||||
const linkText = getDisplayUrl(bookingUrl, username, slug);
|
||||
return (
|
||||
<View className="mb-1 flex-row flex-wrap items-baseline">
|
||||
<Text className="text-base font-semibold text-cal-text">{title}</Text>
|
||||
<Text className="ml-1 text-sm text-cal-text-secondary">{linkText}</Text>
|
||||
<Text className="text-base font-semibold text-cal-text dark:text-white">{title}</Text>
|
||||
<Text className="ml-1 text-sm text-cal-text-secondary dark:text-[#A3A3A3]">{linkText}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -45,7 +45,10 @@ interface EventTypeDescriptionProps {
|
||||
export function EventTypeDescription({ normalizedDescription }: EventTypeDescriptionProps) {
|
||||
if (!normalizedDescription) return null;
|
||||
return (
|
||||
<Text className="mb-2 mt-0.5 text-sm leading-5 text-cal-text-secondary" numberOfLines={2}>
|
||||
<Text
|
||||
className="mb-2 mt-0.5 text-sm leading-5 text-cal-text-secondary dark:text-[#A3A3A3]"
|
||||
numberOfLines={2}
|
||||
>
|
||||
{normalizedDescription}
|
||||
</Text>
|
||||
);
|
||||
@@ -59,7 +62,11 @@ interface EventTypeLinkProps {
|
||||
|
||||
export function EventTypeLink({ username, slug, bookingUrl }: EventTypeLinkProps) {
|
||||
const linkText = getDisplayUrl(bookingUrl, username, slug);
|
||||
return <Text className="mb-1 mt-0.5 text-sm text-cal-text-secondary">{linkText}</Text>;
|
||||
return (
|
||||
<Text className="mb-1 mt-0.5 text-sm text-cal-text-secondary dark:text-[#A3A3A3]">
|
||||
{linkText}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
interface EventTypeBadgesProps {
|
||||
@@ -91,6 +98,10 @@ export function EventTypeBadges({
|
||||
confirmationPolicy,
|
||||
recurrence,
|
||||
}: EventTypeBadgesProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const badgeIconColor = isDark ? "#FFFFFF" : "#000000";
|
||||
|
||||
const hasSeats = seats && !seats.disabled && seats.seatsPerTimeSlot && seats.seatsPerTimeSlot > 0;
|
||||
|
||||
const requiresConfirmation =
|
||||
@@ -118,11 +129,11 @@ export function EventTypeBadges({
|
||||
<View className="mt-2 flex-row flex-wrap items-center gap-2" style={{ width: "100%" }}>
|
||||
{/* Duration Badge */}
|
||||
<View
|
||||
className="rounded-md border border-cal-border bg-cal-border"
|
||||
className="rounded-md border border-cal-border bg-cal-border dark:border-[#4D4D4D] dark:bg-[#4D4D4D]"
|
||||
style={{ height: 24, paddingHorizontal: 8, flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Ionicons name="time-outline" size={14} color="#000000" />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-cal-brand-black">
|
||||
<Ionicons name="time-outline" size={14} color={badgeIconColor} />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-cal-brand-black dark:text-white">
|
||||
{formattedDuration}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -130,22 +141,24 @@ export function EventTypeBadges({
|
||||
{/* Hidden Badge */}
|
||||
{hidden ? (
|
||||
<View
|
||||
className="rounded-md border border-cal-border bg-cal-border"
|
||||
className="rounded-md border border-cal-border bg-cal-border dark:border-[#4D4D4D] dark:bg-[#4D4D4D]"
|
||||
style={{ height: 24, paddingHorizontal: 8, flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Ionicons name="eye-off-outline" size={14} color="#000000" />
|
||||
<Text className="ml-1.5 text-xs font-medium text-cal-brand-black">Hidden</Text>
|
||||
<Ionicons name="eye-off-outline" size={14} color={badgeIconColor} />
|
||||
<Text className="ml-1.5 text-xs font-medium text-cal-brand-black dark:text-white">
|
||||
Hidden
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Seats Badge */}
|
||||
{hasSeats ? (
|
||||
<View
|
||||
className="rounded-md border border-cal-border bg-cal-border"
|
||||
className="rounded-md border border-cal-border bg-cal-border dark:border-[#4D4D4D] dark:bg-[#4D4D4D]"
|
||||
style={{ height: 24, paddingHorizontal: 8, flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Ionicons name="people-outline" size={14} color="#000000" />
|
||||
<Text className="ml-1.5 text-xs font-medium text-cal-brand-black">
|
||||
<Ionicons name="people-outline" size={14} color={badgeIconColor} />
|
||||
<Text className="ml-1.5 text-xs font-medium text-cal-brand-black dark:text-white">
|
||||
{seats.seatsPerTimeSlot} seats
|
||||
</Text>
|
||||
</View>
|
||||
@@ -154,11 +167,11 @@ export function EventTypeBadges({
|
||||
{/* Price Badge */}
|
||||
{hasPrice && formattedPrice ? (
|
||||
<View
|
||||
className="rounded-md border border-cal-border bg-cal-border"
|
||||
className="rounded-md border border-cal-border bg-cal-border dark:border-[#4D4D4D] dark:bg-[#4D4D4D]"
|
||||
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">
|
||||
<Ionicons name="card-outline" size={14} color={badgeIconColor} />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-cal-brand-black dark:text-white">
|
||||
{formattedPrice}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -167,11 +180,11 @@ export function EventTypeBadges({
|
||||
{/* Repeats Badge */}
|
||||
{hasRecurrence ? (
|
||||
<View
|
||||
className="rounded-md border border-cal-border bg-cal-border"
|
||||
className="rounded-md border border-cal-border bg-cal-border dark:border-[#4D4D4D] dark:bg-[#4D4D4D]"
|
||||
style={{ height: 24, paddingHorizontal: 8, flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Ionicons name="repeat-outline" size={14} color="#000000" />
|
||||
<Text className="ml-1.5 text-xs font-medium text-cal-brand-black">
|
||||
<Ionicons name="repeat-outline" size={14} color={badgeIconColor} />
|
||||
<Text className="ml-1.5 text-xs font-medium text-cal-brand-black dark:text-white">
|
||||
{recurrence.occurrences} times
|
||||
</Text>
|
||||
</View>
|
||||
@@ -180,11 +193,11 @@ export function EventTypeBadges({
|
||||
{/* Requires Confirmation Badge */}
|
||||
{requiresConfirmation ? (
|
||||
<View
|
||||
className="rounded-md border border-cal-border bg-cal-border"
|
||||
className="rounded-md border border-cal-border bg-cal-border dark:border-[#4D4D4D] dark:bg-[#4D4D4D]"
|
||||
style={{ height: 24, paddingHorizontal: 8, flexDirection: "row", alignItems: "center" }}
|
||||
>
|
||||
<Ionicons name="checkmark-circle-outline" size={14} color="#000000" />
|
||||
<Text className="ml-1.5 text-xs font-medium text-cal-brand-black">
|
||||
<Ionicons name="checkmark-circle-outline" size={14} color={badgeIconColor} />
|
||||
<Text className="ml-1.5 text-xs font-medium text-cal-brand-black dark:text-white">
|
||||
Requires confirmation
|
||||
</Text>
|
||||
</View>
|
||||
@@ -208,20 +221,25 @@ export function EventTypeActions({
|
||||
handlePreview,
|
||||
handleEventTypeLongPress,
|
||||
}: EventTypeActionsProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const iconColor = isDark ? "#FFFFFF" : "#3C3F44";
|
||||
const svgStrokeColor = isDark ? "#FFFFFF" : "#3C3F44";
|
||||
|
||||
return (
|
||||
<View className="flex-row">
|
||||
<Tooltip text="Preview">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-l-lg border border-r-0 border-cal-border"
|
||||
className="items-center justify-center rounded-l-lg border border-r-0 border-cal-border dark:border-[#4D4D4D] dark:bg-[#171717]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handlePreview(item)}
|
||||
>
|
||||
<Ionicons name="open-outline" size={18} color="#3C3F44" />
|
||||
<Ionicons name="open-outline" size={18} color={iconColor} />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<Tooltip text={copiedEventTypeId === item.id ? "Copied!" : "Copy link"}>
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center border border-r-0 border-cal-border"
|
||||
className="items-center justify-center border border-r-0 border-cal-border dark:border-[#4D4D4D] dark:bg-[#171717]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleCopyLink(item)}
|
||||
>
|
||||
@@ -233,7 +251,7 @@ export function EventTypeActions({
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#3C3F44"
|
||||
stroke={svgStrokeColor}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
@@ -246,11 +264,11 @@ export function EventTypeActions({
|
||||
</Tooltip>
|
||||
<Tooltip text="More">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-r-lg border border-cal-border"
|
||||
className="items-center justify-center rounded-r-lg border border-cal-border dark:border-[#4D4D4D] dark:bg-[#171717]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleEventTypeLongPress(item)}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color={iconColor} />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
</View>
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
import { View } from "react-native";
|
||||
import { View, useColorScheme } from "react-native";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface EventTypeListItemSkeletonProps {
|
||||
isLast?: boolean;
|
||||
}
|
||||
|
||||
export function EventTypeListItemSkeleton({ isLast = false }: EventTypeListItemSkeletonProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
return (
|
||||
<View className={`bg-cal-bg ${!isLast ? "border-b border-cal-border" : ""}`}>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: theme.background,
|
||||
borderBottomWidth: !isLast ? 1 : 0,
|
||||
borderBottomColor: theme.border,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: 16,
|
||||
@@ -50,8 +61,8 @@ export function EventTypeListItemSkeleton({ isLast = false }: EventTypeListItemS
|
||||
alignItems: "center",
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: "#E5E5EA",
|
||||
backgroundColor: "#F5F5F5",
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.backgroundMuted,
|
||||
}}
|
||||
>
|
||||
<Skeleton style={{ height: 14, width: 14, borderRadius: 3 }} />
|
||||
@@ -67,8 +78,8 @@ export function EventTypeListItemSkeleton({ isLast = false }: EventTypeListItemS
|
||||
alignItems: "center",
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: "#E5E5EA",
|
||||
backgroundColor: "#F5F5F5",
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.backgroundMuted,
|
||||
}}
|
||||
>
|
||||
<Skeleton style={{ height: 14, width: 14, borderRadius: 3 }} />
|
||||
@@ -84,8 +95,8 @@ export function EventTypeListItemSkeleton({ isLast = false }: EventTypeListItemS
|
||||
alignItems: "center",
|
||||
borderRadius: 6,
|
||||
borderWidth: 1,
|
||||
borderColor: "#E5E5EA",
|
||||
backgroundColor: "#F5F5F5",
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.backgroundMuted,
|
||||
}}
|
||||
>
|
||||
<Skeleton style={{ height: 14, width: 14, borderRadius: 3 }} />
|
||||
@@ -106,9 +117,19 @@ interface EventTypeListSkeletonProps {
|
||||
}
|
||||
|
||||
export function EventTypeListSkeleton({ count = 5 }: EventTypeListSkeletonProps) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
return (
|
||||
<View className="px-2 pt-4 md:px-4">
|
||||
<View className="overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
<View
|
||||
className="overflow-hidden rounded-lg border"
|
||||
style={{
|
||||
borderColor: theme.border,
|
||||
backgroundColor: theme.background,
|
||||
}}
|
||||
>
|
||||
{Array.from({ length: count }).map((_, index) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Static skeleton items don't reorder
|
||||
<EventTypeListItemSkeleton key={`skeleton-${index}`} isLast={index === count - 1} />
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@@ -22,6 +23,7 @@ import { useAddGuests } from "@/hooks/useBookings";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
|
||||
import { safeLogError } from "@/utils/safeLogger";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
export interface AddGuestsScreenProps {
|
||||
booking: Booking | null;
|
||||
@@ -47,8 +49,28 @@ export const AddGuestsScreen = forwardRef<AddGuestsScreenHandle, AddGuestsScreen
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#F2F2F7",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
borderLight: isDark ? "#2C2C2E" : "rgba(243, 244, 246, 0.5)",
|
||||
pill: isDark ? "#4D4D4D" : "#E8E8ED",
|
||||
pillTransparent: isDark ? "rgba(56, 56, 58, 0.5)" : "rgba(232, 232, 237, 0.5)",
|
||||
inputBorder: isDark ? "rgba(56, 56, 58, 0.4)" : "rgba(209, 213, 219, 0.4)",
|
||||
inputBackground: isDark ? "rgba(28, 28, 30, 0.6)" : "rgba(255, 255, 255, 0.6)",
|
||||
accent: "#007AFF",
|
||||
destructive: getColors(isDark).destructive,
|
||||
};
|
||||
|
||||
// Derive styles from colors and props
|
||||
const backgroundStyle = transparentBackground ? "" : `bg-[${colors.background}]`;
|
||||
const pillStyle = transparentBackground ? "bg-gray-200/50" : `bg-[${colors.pill}]`;
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [guests, setGuests] = useState<{ email: string; name?: string }[]>([]);
|
||||
@@ -160,20 +182,30 @@ export const AddGuestsScreen = forwardRef<AddGuestsScreenHandle, AddGuestsScreen
|
||||
>
|
||||
{/* Email input */}
|
||||
{transparentBackground && (
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-gray-500">Email *</Text>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#A3A3A3]">Email *</Text>
|
||||
)}
|
||||
<View
|
||||
className={`mb-3 overflow-hidden rounded-xl px-4 py-3 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
{!transparentBackground && (
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">Email *</Text>
|
||||
<Text
|
||||
className={`mb-1.5 text-[13px] font-medium ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
Email *
|
||||
</Text>
|
||||
)}
|
||||
<TextInput
|
||||
className="h-10 text-[17px] text-[#000]"
|
||||
className={`h-10 text-[17px] ${isDark ? "text-white" : "text-[#000]"}`}
|
||||
placeholder="guest@example.com"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={email}
|
||||
onChangeText={setEmail}
|
||||
keyboardType="email-address"
|
||||
@@ -185,20 +217,32 @@ export const AddGuestsScreen = forwardRef<AddGuestsScreenHandle, AddGuestsScreen
|
||||
|
||||
{/* Name input */}
|
||||
{transparentBackground && (
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-gray-500">Name (optional)</Text>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#A3A3A3]">
|
||||
Name (optional)
|
||||
</Text>
|
||||
)}
|
||||
<View
|
||||
className={`mb-4 overflow-hidden rounded-xl px-4 py-3 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
{!transparentBackground && (
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">Name (optional)</Text>
|
||||
<Text
|
||||
className={`mb-1.5 text-[13px] font-medium ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
Name (optional)
|
||||
</Text>
|
||||
)}
|
||||
<TextInput
|
||||
className="h-10 text-[17px] text-[#000]"
|
||||
className={`h-10 text-[17px] ${isDark ? "text-white" : "text-[#000]"}`}
|
||||
placeholder="Guest Name"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
editable={!isSaving}
|
||||
@@ -208,7 +252,13 @@ export const AddGuestsScreen = forwardRef<AddGuestsScreenHandle, AddGuestsScreen
|
||||
{/* Add button */}
|
||||
<TouchableOpacity
|
||||
className={`mb-6 flex-row items-center justify-center rounded-xl py-3 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
onPress={handleAddGuest}
|
||||
activeOpacity={0.7}
|
||||
@@ -221,30 +271,48 @@ export const AddGuestsScreen = forwardRef<AddGuestsScreenHandle, AddGuestsScreen
|
||||
{/* Guest list */}
|
||||
{guests.length > 0 && (
|
||||
<View>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-gray-500">
|
||||
<Text
|
||||
className={`mb-2 px-1 text-[13px] font-medium uppercase tracking-wide ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
Guests to add ({guests.length})
|
||||
</Text>
|
||||
<View
|
||||
className={`overflow-hidden rounded-xl ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
{guests.map((guest, index) => (
|
||||
<View
|
||||
key={guest.email}
|
||||
className={`flex-row items-center px-4 py-3 ${
|
||||
index < guests.length - 1 ? "border-b border-gray-100/50" : ""
|
||||
index < guests.length - 1
|
||||
? isDark
|
||||
? "border-b border-[#4D4D4D]/50"
|
||||
: "border-b border-gray-100/50"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-10 w-10 items-center justify-center rounded-full ${pillStyle}`}
|
||||
>
|
||||
<Ionicons name="person" size={20} color="#6B7280" />
|
||||
<Ionicons name="person" size={20} color={isDark ? "#A3A3A3" : "#6B7280"} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[17px] text-[#000]">{guest.email}</Text>
|
||||
<Text className={`text-[17px] ${isDark ? "text-white" : "text-[#000]"}`}>
|
||||
{guest.email}
|
||||
</Text>
|
||||
{guest.name && (
|
||||
<Text className="mt-0.5 text-[15px] text-gray-500">{guest.name}</Text>
|
||||
<Text
|
||||
className={`mt-0.5 text-[15px] ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
{guest.name}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<TouchableOpacity
|
||||
@@ -252,7 +320,7 @@ export const AddGuestsScreen = forwardRef<AddGuestsScreenHandle, AddGuestsScreen
|
||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<Ionicons name="close-circle-outline" size={24} color="#800020" />
|
||||
<Ionicons name="close-circle-outline" size={24} color={colors.destructive} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
))}
|
||||
|
||||
@@ -9,7 +9,15 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo } from "react";
|
||||
import { ActivityIndicator, Alert, RefreshControl, ScrollView, Text, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Text,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { useDeleteSchedule, useScheduleById, useSetScheduleAsDefault } from "@/hooks/useSchedules";
|
||||
@@ -69,6 +77,20 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
>(function AvailabilityDetailScreen({ id, onActionsReady }, ref) {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#f2f2f7",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#A3A3A3",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
chevron: isDark ? "#636366" : "#C7C7CC",
|
||||
enabledDot: "#34C759",
|
||||
disabledDot: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
destructive: isDark ? "#FF453A" : "#FF3B30",
|
||||
};
|
||||
|
||||
// Use React Query hook for data fetching and cache synchronization
|
||||
const { data: schedule, isLoading, error, refetch, isRefetching } = useScheduleById(Number(id));
|
||||
@@ -227,15 +249,20 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-[#f2f2f7]">
|
||||
<ActivityIndicator size="large" color="#000000" />
|
||||
<Text className="mt-4 text-[15px] text-[#8E8E93]">Loading availability...</Text>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: colors.background }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={colors.text} />
|
||||
<Text className="mt-4 text-[15px]" style={{ color: colors.textSecondary }}>
|
||||
Loading availability...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-[#f2f2f7]">
|
||||
<View className="flex-1" style={{ backgroundColor: colors.background }}>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{
|
||||
@@ -250,8 +277,8 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
<View className="mb-4">
|
||||
<View className="mb-2 flex-row flex-wrap items-center">
|
||||
<Text
|
||||
className="text-[26px] font-semibold leading-tight text-black"
|
||||
style={{ letterSpacing: -0.3 }}
|
||||
className="text-[26px] font-semibold leading-tight"
|
||||
style={{ letterSpacing: -0.3, color: colors.text }}
|
||||
>
|
||||
{scheduleName || "Untitled Schedule"}
|
||||
</Text>
|
||||
@@ -259,7 +286,7 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
<Ionicons
|
||||
name="star-outline"
|
||||
size={20}
|
||||
color="#000000"
|
||||
color={colors.text}
|
||||
style={{ marginLeft: 6, marginTop: 2 }}
|
||||
/>
|
||||
)}
|
||||
@@ -267,20 +294,28 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
</View>
|
||||
|
||||
{/* Weekly Schedule Card - Navigable */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<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>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
Weekly Schedule
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">
|
||||
<Text className="mr-1 text-[17px]" style={{ color: colors.textSecondary }}>
|
||||
{enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Expanded Day List */}
|
||||
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
|
||||
<View
|
||||
className="px-4 py-2.5"
|
||||
style={{ borderTopWidth: 1, borderTopColor: colors.border }}
|
||||
>
|
||||
{DAYS.map((day, dayIndex) => {
|
||||
const daySlots = availability[dayIndex] || [];
|
||||
const isEnabled = daySlots.length > 0;
|
||||
@@ -288,19 +323,18 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
return (
|
||||
<View
|
||||
key={day}
|
||||
className={`flex-row items-center py-3 ${
|
||||
dayIndex > 0 ? "border-t border-[#E5E5EA]" : ""
|
||||
}`}
|
||||
className="flex-row items-center py-3"
|
||||
style={dayIndex > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}}
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-2.5 w-2.5 rounded-full ${
|
||||
isEnabled ? "bg-[#34C759]" : "bg-[#E5E5EA]"
|
||||
}`}
|
||||
className="mr-3 h-2.5 w-2.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: isEnabled ? colors.enabledDot : colors.disabledDot,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
className={`w-24 text-[15px] font-medium ${
|
||||
isEnabled ? "text-black" : "text-[#8E8E93]"
|
||||
}`}
|
||||
className="w-24 text-[15px] font-medium"
|
||||
style={{ color: isEnabled ? colors.text : colors.textSecondary }}
|
||||
>
|
||||
{DAYS[dayIndex]}
|
||||
</Text>
|
||||
@@ -310,14 +344,18 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
{daySlots.map((slot, slotIndex) => (
|
||||
<Text
|
||||
key={`${slotIndex}-${slot.startTime}`}
|
||||
className={`text-[15px] text-black ${slotIndex > 0 ? "mt-1" : ""}`}
|
||||
className={`text-[15px] ${slotIndex > 0 ? "mt-1" : ""}`}
|
||||
style={{ color: colors.text }}
|
||||
>
|
||||
{formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="flex-1 text-right text-[15px] text-[#8E8E93]">
|
||||
<Text
|
||||
className="flex-1 text-right text-[15px]"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Unavailable
|
||||
</Text>
|
||||
)}
|
||||
@@ -329,13 +367,20 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
</View>
|
||||
|
||||
{/* Timezone Card - Navigable */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<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>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
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" />
|
||||
<Text className="mr-1 text-[17px]" style={{ color: colors.textSecondary }}>
|
||||
{timeZone}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
@@ -343,34 +388,45 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
|
||||
{/* Date Overrides Card - Navigable */}
|
||||
{overrides.length > 0 && (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<AppPressable
|
||||
onPress={() => router.push(`/edit-availability-override?id=${id}` as never)}
|
||||
>
|
||||
<View className="flex-row items-center justify-between px-4 py-3.5">
|
||||
<Text className="text-[17px] text-black">Date Overrides</Text>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
Date Overrides
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">
|
||||
<Text className="mr-1 text-[17px]" style={{ color: colors.textSecondary }}>
|
||||
{overrides.length} {overrides.length === 1 ? "override" : "overrides"}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Expanded Overrides List */}
|
||||
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
|
||||
<View
|
||||
className="px-4 py-2.5"
|
||||
style={{ borderTopWidth: 1, borderTopColor: colors.border }}
|
||||
>
|
||||
{overrides.map((override, index) => (
|
||||
<View
|
||||
key={override.date}
|
||||
className={`py-2.5 ${index > 0 ? "border-t border-[#E5E5EA]" : ""}`}
|
||||
className="py-2.5"
|
||||
style={index > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}}
|
||||
>
|
||||
<Text className="mb-0.5 text-[17px] text-black">
|
||||
<Text className="mb-0.5 text-[17px]" style={{ color: colors.text }}>
|
||||
{formatDateForDisplay(override.date)}
|
||||
</Text>
|
||||
{override.startTime === "00:00" && override.endTime === "00:00" ? (
|
||||
<Text className="text-[15px] text-[#FF3B30]">Unavailable</Text>
|
||||
<Text className="text-[15px]" style={{ color: colors.destructive }}>
|
||||
Unavailable
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-[15px] text-[#8E8E93]">
|
||||
<Text className="text-[15px]" style={{ color: colors.textSecondary }}>
|
||||
{formatTime12Hour(`${override.startTime}:00`)} –{" "}
|
||||
{formatTime12Hour(`${override.endTime}:00`)}
|
||||
</Text>
|
||||
@@ -384,16 +440,23 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
|
||||
{/* No Overrides Message - Still navigable to add overrides */}
|
||||
{overrides.length === 0 && (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<AppPressable
|
||||
onPress={() => router.push(`/edit-availability-override?id=${id}` as never)}
|
||||
>
|
||||
<View className="flex-row items-center justify-between px-4 py-3.5">
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">Date Overrides</Text>
|
||||
<Text className="mt-1 text-[15px] text-[#8E8E93]">No date overrides set</Text>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
Date Overrides
|
||||
</Text>
|
||||
<Text className="mt-1 text-[15px]" style={{ color: colors.textSecondary }}>
|
||||
No date overrides set
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</View>
|
||||
</AppPressable>
|
||||
</View>
|
||||
|
||||
@@ -9,7 +9,15 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo } from "react";
|
||||
import { ActivityIndicator, Alert, RefreshControl, ScrollView, Text, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Text,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { useDeleteSchedule, useScheduleById, useSetScheduleAsDefault } from "@/hooks/useSchedules";
|
||||
@@ -69,6 +77,20 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
>(function AvailabilityDetailScreen({ id, onActionsReady }, ref) {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#f2f2f7",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#A3A3A3",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
chevron: isDark ? "#636366" : "#C7C7CC",
|
||||
enabledDot: "#34C759",
|
||||
disabledDot: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
destructive: isDark ? "#FF453A" : "#FF3B30",
|
||||
};
|
||||
|
||||
// Use React Query hook for data fetching and cache synchronization
|
||||
const { data: schedule, isLoading, error, refetch, isRefetching } = useScheduleById(Number(id));
|
||||
@@ -227,15 +249,20 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-[#f2f2f7]">
|
||||
<ActivityIndicator size="large" color="#000000" />
|
||||
<Text className="mt-4 text-[15px] text-[#8E8E93]">Loading availability...</Text>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: colors.background }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={colors.text} />
|
||||
<Text className="mt-4 text-[15px]" style={{ color: colors.textSecondary }}>
|
||||
Loading availability...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-[#f2f2f7]">
|
||||
<View className="flex-1" style={{ backgroundColor: colors.background }}>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{
|
||||
@@ -250,8 +277,8 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
<View className="mb-4">
|
||||
<View className="mb-2 flex-row flex-wrap items-center">
|
||||
<Text
|
||||
className="text-[26px] font-semibold leading-tight text-black"
|
||||
style={{ letterSpacing: -0.3 }}
|
||||
className="text-[26px] font-semibold leading-tight"
|
||||
style={{ letterSpacing: -0.3, color: colors.text }}
|
||||
>
|
||||
{scheduleName || "Untitled Schedule"}
|
||||
</Text>
|
||||
@@ -259,7 +286,7 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
<Ionicons
|
||||
name="star-outline"
|
||||
size={20}
|
||||
color="#000000"
|
||||
color={colors.text}
|
||||
style={{ marginLeft: 6, marginTop: 2 }}
|
||||
/>
|
||||
)}
|
||||
@@ -267,20 +294,28 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
</View>
|
||||
|
||||
{/* Weekly Schedule Card - Navigable */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<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>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
Weekly Schedule
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">
|
||||
<Text className="mr-1 text-[17px]" style={{ color: colors.textSecondary }}>
|
||||
{enabledDaysCount} {enabledDaysCount === 1 ? "day" : "days"}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Expanded Day List */}
|
||||
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
|
||||
<View
|
||||
className="px-4 py-2.5"
|
||||
style={{ borderTopWidth: 1, borderTopColor: colors.border }}
|
||||
>
|
||||
{DAYS.map((day, dayIndex) => {
|
||||
const daySlots = availability[dayIndex] || [];
|
||||
const isEnabled = daySlots.length > 0;
|
||||
@@ -288,19 +323,18 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
return (
|
||||
<View
|
||||
key={day}
|
||||
className={`flex-row items-center py-3 ${
|
||||
dayIndex > 0 ? "border-t border-[#E5E5EA]" : ""
|
||||
}`}
|
||||
className="flex-row items-center py-3"
|
||||
style={dayIndex > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}}
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-2.5 w-2.5 rounded-full ${
|
||||
isEnabled ? "bg-[#34C759]" : "bg-[#E5E5EA]"
|
||||
}`}
|
||||
className="mr-3 h-2.5 w-2.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: isEnabled ? colors.enabledDot : colors.disabledDot,
|
||||
}}
|
||||
/>
|
||||
<Text
|
||||
className={`w-24 text-[15px] font-medium ${
|
||||
isEnabled ? "text-black" : "text-[#8E8E93]"
|
||||
}`}
|
||||
className="w-24 text-[15px] font-medium"
|
||||
style={{ color: isEnabled ? colors.text : colors.textSecondary }}
|
||||
>
|
||||
{DAYS[dayIndex]}
|
||||
</Text>
|
||||
@@ -310,14 +344,18 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
{daySlots.map((slot, slotIndex) => (
|
||||
<Text
|
||||
key={`${slotIndex}-${slot.startTime}`}
|
||||
className={`text-[15px] text-black ${slotIndex > 0 ? "mt-1" : ""}`}
|
||||
className={`text-[15px] ${slotIndex > 0 ? "mt-1" : ""}`}
|
||||
style={{ color: colors.text }}
|
||||
>
|
||||
{formatTime12Hour(slot.startTime)} - {formatTime12Hour(slot.endTime)}
|
||||
</Text>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="flex-1 text-right text-[15px] text-[#8E8E93]">
|
||||
<Text
|
||||
className="flex-1 text-right text-[15px]"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Unavailable
|
||||
</Text>
|
||||
)}
|
||||
@@ -329,13 +367,20 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
</View>
|
||||
|
||||
{/* Timezone Card - Navigable */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<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>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
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" />
|
||||
<Text className="mr-1 text-[17px]" style={{ color: colors.textSecondary }}>
|
||||
{timeZone}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
@@ -343,34 +388,45 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
|
||||
{/* Date Overrides Card - Navigable */}
|
||||
{overrides.length > 0 && (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<AppPressable
|
||||
onPress={() => router.push(`/edit-availability-override?id=${id}` as never)}
|
||||
>
|
||||
<View className="flex-row items-center justify-between px-4 py-3.5">
|
||||
<Text className="text-[17px] text-black">Date Overrides</Text>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
Date Overrides
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">
|
||||
<Text className="mr-1 text-[17px]" style={{ color: colors.textSecondary }}>
|
||||
{overrides.length} {overrides.length === 1 ? "override" : "overrides"}
|
||||
</Text>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Expanded Overrides List */}
|
||||
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
|
||||
<View
|
||||
className="px-4 py-2.5"
|
||||
style={{ borderTopWidth: 1, borderTopColor: colors.border }}
|
||||
>
|
||||
{overrides.map((override, index) => (
|
||||
<View
|
||||
key={override.date}
|
||||
className={`py-2.5 ${index > 0 ? "border-t border-[#E5E5EA]" : ""}`}
|
||||
className="py-2.5"
|
||||
style={index > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}}
|
||||
>
|
||||
<Text className="mb-0.5 text-[17px] text-black">
|
||||
<Text className="mb-0.5 text-[17px]" style={{ color: colors.text }}>
|
||||
{formatDateForDisplay(override.date)}
|
||||
</Text>
|
||||
{override.startTime === "00:00" && override.endTime === "00:00" ? (
|
||||
<Text className="text-[15px] text-[#FF3B30]">Unavailable</Text>
|
||||
<Text className="text-[15px]" style={{ color: colors.destructive }}>
|
||||
Unavailable
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-[15px] text-[#8E8E93]">
|
||||
<Text className="text-[15px]" style={{ color: colors.textSecondary }}>
|
||||
{formatTime12Hour(`${override.startTime}:00`)} -{" "}
|
||||
{formatTime12Hour(`${override.endTime}:00`)}
|
||||
</Text>
|
||||
@@ -384,16 +440,23 @@ export const AvailabilityDetailScreen = forwardRef<
|
||||
|
||||
{/* No Overrides Message - Still navigable to add overrides */}
|
||||
{overrides.length === 0 && (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<AppPressable
|
||||
onPress={() => router.push(`/edit-availability-override?id=${id}` as never)}
|
||||
>
|
||||
<View className="flex-row items-center justify-between px-4 py-3.5">
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">Date Overrides</Text>
|
||||
<Text className="mt-1 text-[15px] text-[#8E8E93]">No date overrides set</Text>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
Date Overrides
|
||||
</Text>
|
||||
<Text className="mt-1 text-[15px]" style={{ color: colors.textSecondary }}>
|
||||
No date overrides set
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</View>
|
||||
</AppPressable>
|
||||
</View>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
|
||||
import { offlineAwareRefresh } from "@/utils/network";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
export interface AvailabilityListScreenProps {
|
||||
searchQuery: string;
|
||||
@@ -61,6 +63,18 @@ export function AvailabilityListScreen({
|
||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||
const [isManualRefreshing, setIsManualRefreshing] = useState(false);
|
||||
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#FFFFFF",
|
||||
backgroundSecondary: isDark ? "#171717" : "#f8f9fa",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
text: isDark ? "#FFFFFF" : "#333333",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#666666",
|
||||
};
|
||||
|
||||
// Use React Query hooks
|
||||
const { data: schedules = [], isLoading: loading, error: queryError, refetch } = useSchedules();
|
||||
|
||||
@@ -249,7 +263,7 @@ export function AvailabilityListScreen({
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="flex-1 bg-[#f8f9fa]">
|
||||
<View style={{ flex: 1, backgroundColor: colors.backgroundSecondary }}>
|
||||
<Header />
|
||||
<View className="flex-1 px-2 pt-4 md:px-4">
|
||||
<AvailabilityListSkeleton iosStyle={Platform.OS === "ios"} />
|
||||
@@ -260,16 +274,21 @@ export function AvailabilityListScreen({
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View className="flex-1 bg-[#f8f9fa]">
|
||||
<View style={{ flex: 1, backgroundColor: colors.backgroundSecondary }}>
|
||||
<Header />
|
||||
<View className="flex-1 items-center justify-center p-5">
|
||||
<Ionicons name="alert-circle" size={64} color="#800020" />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-[#333]">
|
||||
<Ionicons name="alert-circle" size={64} color={theme.destructive} />
|
||||
<Text style={{ color: colors.text }} className="mb-2 mt-4 text-center text-xl font-bold">
|
||||
Unable to load availability
|
||||
</Text>
|
||||
<Text className="mb-6 text-center text-base text-[#666]">{error}</Text>
|
||||
<AppPressable className="rounded-lg bg-black px-6 py-3" onPress={() => refetch()}>
|
||||
<Text className="text-base font-semibold text-white">Retry</Text>
|
||||
<Text style={{ color: colors.textSecondary }} className="mb-6 text-center text-base">
|
||||
{error}
|
||||
</Text>
|
||||
<AppPressable
|
||||
className="rounded-lg bg-black px-6 py-3 dark:bg-white"
|
||||
onPress={() => refetch()}
|
||||
>
|
||||
<Text className="text-base font-semibold text-white dark:text-black">Retry</Text>
|
||||
</AppPressable>
|
||||
</View>
|
||||
</View>
|
||||
@@ -287,11 +306,23 @@ export function AvailabilityListScreen({
|
||||
{/* Non-iOS header with search */}
|
||||
<Activity mode={Platform.OS !== "ios" ? "visible" : "hidden"}>
|
||||
<Header />
|
||||
<View className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2">
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: isDark ? "#000000" : "#f3f4f6",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
}}
|
||||
className="flex-row items-center gap-3 px-4 py-2"
|
||||
>
|
||||
<TextInput
|
||||
className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-[17px] text-black focus:border-black focus:ring-2 focus:ring-black"
|
||||
style={{
|
||||
backgroundColor: isDark ? "#171717" : "#FFFFFF",
|
||||
borderColor: colors.border,
|
||||
color: colors.text,
|
||||
}}
|
||||
className="flex-1 rounded-lg border px-3 py-2 text-[17px]"
|
||||
placeholder="Search schedules"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
autoCapitalize="none"
|
||||
@@ -299,11 +330,11 @@ export function AvailabilityListScreen({
|
||||
clearButtonMode="while-editing"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
|
||||
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2 dark:bg-white"
|
||||
onPress={handleCreateNew}
|
||||
>
|
||||
<Ionicons name="add" size={18} color="#fff" />
|
||||
<Text className="text-base font-semibold text-white">New</Text>
|
||||
<Ionicons name="add" size={18} color={isDark ? "#000" : "#fff"} />
|
||||
<Text className="text-base font-semibold text-white dark:text-black">New</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Activity>
|
||||
@@ -311,7 +342,7 @@ export function AvailabilityListScreen({
|
||||
{/* Empty state - no schedules */}
|
||||
<Activity mode={showEmptyState ? "visible" : "hidden"}>
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: colors.background }}
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
@@ -337,7 +368,7 @@ export function AvailabilityListScreen({
|
||||
{/* Search empty state */}
|
||||
<Activity mode={showSearchEmptyState ? "visible" : "hidden"}>
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
style={{ backgroundColor: colors.background }}
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
@@ -363,7 +394,13 @@ export function AvailabilityListScreen({
|
||||
<AvailabilityListSkeleton iosStyle={Platform.OS === "ios"} />
|
||||
) : (
|
||||
<FlatList
|
||||
className="flex-1 rounded-lg border border-[#E5E5EA] bg-white"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.background,
|
||||
}}
|
||||
contentContainerStyle={{
|
||||
paddingBottom: 90,
|
||||
paddingHorizontal: 8,
|
||||
@@ -455,24 +492,28 @@ export function AvailabilityListScreen({
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-black/50 p-4">
|
||||
<TouchableOpacity
|
||||
className="w-full max-w-md rounded-2xl bg-white shadow-2xl"
|
||||
className="w-full max-w-md rounded-2xl bg-white shadow-2xl dark:bg-[#171717]"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="p-6 pb-2">
|
||||
<Text className="mb-2 text-2xl font-semibold text-gray-900">
|
||||
<Text className="mb-2 text-2xl font-semibold text-gray-900 dark:text-white">
|
||||
Add a new schedule
|
||||
</Text>
|
||||
<Text className="text-sm text-gray-500">Create a new availability schedule.</Text>
|
||||
<Text className="text-sm text-gray-500 dark:text-[#A3A3A3]">
|
||||
Create a new availability schedule.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Content */}
|
||||
<View className="px-6 pb-4">
|
||||
<View className="mb-1">
|
||||
<Text className="mb-2 text-sm font-medium text-gray-700">Name</Text>
|
||||
<Text className="mb-2 text-sm font-medium text-gray-700 dark:text-[#A3A3A3]">
|
||||
Name
|
||||
</Text>
|
||||
<TextInput
|
||||
className="rounded-md border border-gray-300 bg-white px-3 py-2.5 text-base text-gray-900"
|
||||
className="rounded-md border border-gray-300 bg-white px-3 py-2.5 text-base text-gray-900 dark:border-[#4D4D4D] dark:bg-[#262626] dark:text-white"
|
||||
placeholder="Working Hours"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={newScheduleName}
|
||||
@@ -500,14 +541,16 @@ export function AvailabilityListScreen({
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2.5"
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2.5 dark:border-[#4D4D4D] dark:bg-[#262626]"
|
||||
onPress={() => {
|
||||
onShowCreateModalChange(false);
|
||||
setNewScheduleName("");
|
||||
}}
|
||||
disabled={creating}
|
||||
>
|
||||
<Text className="text-center text-base font-medium text-gray-700">Cancel</Text>
|
||||
<Text className="text-center text-base font-medium text-gray-700 dark:text-white">
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -527,13 +570,13 @@ export function AvailabilityListScreen({
|
||||
onPress={() => setShowActionsModal(false)}
|
||||
>
|
||||
<AppPressable
|
||||
className="mx-4 w-full max-w-sm rounded-2xl bg-white"
|
||||
className="mx-4 w-full max-w-sm rounded-2xl bg-white dark:bg-[#171717]"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="border-b border-gray-200 p-6">
|
||||
<Text className="text-center text-xl font-semibold text-gray-900">
|
||||
<View className="border-b border-gray-200 p-6 dark:border-[#4D4D4D]">
|
||||
<Text className="text-center text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Schedule Actions
|
||||
</Text>
|
||||
</View>
|
||||
@@ -551,13 +594,15 @@ export function AvailabilityListScreen({
|
||||
handleSetAsDefault(selectedSchedule);
|
||||
}
|
||||
}}
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 md:p-4"
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#262626] md:p-4"
|
||||
>
|
||||
<Ionicons name="star-outline" size={20} color="#6B7280" />
|
||||
<Text className="ml-3 text-base text-gray-900">Set as Default</Text>
|
||||
<Text className="ml-3 text-base text-gray-900 dark:text-white">
|
||||
Set as Default
|
||||
</Text>
|
||||
</AppPressable>
|
||||
|
||||
<View className="mx-4 my-2 h-px bg-gray-200" />
|
||||
<View className="mx-4 my-2 h-px bg-gray-200 dark:bg-[#4D4D4D]" />
|
||||
</Activity>
|
||||
|
||||
{/* Duplicate */}
|
||||
@@ -570,14 +615,14 @@ export function AvailabilityListScreen({
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 md:p-4"
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#262626] md:p-4"
|
||||
>
|
||||
<Ionicons name="copy-outline" size={20} color="#6B7280" />
|
||||
<Text className="ml-3 text-base text-gray-900">Duplicate</Text>
|
||||
<Text className="ml-3 text-base text-gray-900 dark:text-white">Duplicate</Text>
|
||||
</AppPressable>
|
||||
|
||||
{/* Separator */}
|
||||
<View className="mx-4 my-2 h-px bg-gray-200" />
|
||||
<View className="mx-4 my-2 h-px bg-gray-200 dark:bg-[#4D4D4D]" />
|
||||
|
||||
{/* Delete */}
|
||||
<AppPressable
|
||||
@@ -590,20 +635,24 @@ export function AvailabilityListScreen({
|
||||
}, 100);
|
||||
}
|
||||
}}
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 md:p-4"
|
||||
className="flex-row items-center p-2 hover:bg-gray-50 dark:hover:bg-[#262626] md:p-4"
|
||||
>
|
||||
<Ionicons name="trash-outline" size={20} color="#800000" />
|
||||
<Text className="ml-3 text-base text-[#800000]">Delete</Text>
|
||||
<Ionicons name="trash-outline" size={20} color={theme.destructive} />
|
||||
<Text className="ml-3 text-base" style={{ color: theme.destructive }}>
|
||||
Delete
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</View>
|
||||
|
||||
{/* Cancel button */}
|
||||
<View className="border-t border-gray-200 p-2 md:p-4">
|
||||
<View className="border-t border-gray-200 p-2 dark:border-[#4D4D4D] md:p-4">
|
||||
<AppPressable
|
||||
className="w-full rounded-lg bg-gray-100 p-3"
|
||||
className="w-full rounded-lg bg-gray-100 p-3 dark:bg-[#262626]"
|
||||
onPress={() => setShowActionsModal(false)}
|
||||
>
|
||||
<Text className="text-center text-base font-medium text-gray-700">Cancel</Text>
|
||||
<Text className="text-center text-base font-medium text-gray-700 dark:text-white">
|
||||
Cancel
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</View>
|
||||
</AppPressable>
|
||||
@@ -617,18 +666,20 @@ export function AvailabilityListScreen({
|
||||
onRequestClose={() => !deleting && setShowDeleteModal(false)}
|
||||
>
|
||||
<View className="flex-1 items-center justify-center bg-black/50 p-4">
|
||||
<View className="w-full max-w-md rounded-2xl bg-white shadow-2xl">
|
||||
<View className="w-full max-w-md rounded-2xl bg-white shadow-2xl dark:bg-[#171717]">
|
||||
<View className="p-6">
|
||||
<View className="flex-row">
|
||||
{/* Danger icon */}
|
||||
<View className="mr-3 self-start rounded-full bg-red-50 p-2">
|
||||
<Ionicons name="alert-circle" size={20} color="#800000" />
|
||||
<View className="mr-3 self-start rounded-full bg-red-50 p-2 dark:bg-red-900/30">
|
||||
<Ionicons name="alert-circle" size={20} color={theme.destructive} />
|
||||
</View>
|
||||
|
||||
{/* Title and description */}
|
||||
<View className="flex-1">
|
||||
<Text className="mb-2 text-xl font-semibold text-gray-900">Delete Schedule</Text>
|
||||
<Text className="text-sm leading-5 text-gray-600">
|
||||
<Text className="mb-2 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Delete Schedule
|
||||
</Text>
|
||||
<Text className="text-sm leading-5 text-gray-600 dark:text-[#A3A3A3]">
|
||||
Are you sure you want to delete "{selectedSchedule?.name}"? This action cannot
|
||||
be undone.
|
||||
</Text>
|
||||
@@ -649,11 +700,13 @@ export function AvailabilityListScreen({
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2.5"
|
||||
className="rounded-lg border border-gray-300 bg-white px-4 py-2.5 dark:border-[#4D4D4D] dark:bg-[#262626]"
|
||||
onPress={() => setShowDeleteModal(false)}
|
||||
disabled={deleting}
|
||||
>
|
||||
<Text className="text-center text-base font-medium text-gray-700">Cancel</Text>
|
||||
<Text className="text-center text-base font-medium text-gray-700 dark:text-white">
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,13 +1,45 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Alert, RefreshControl, ScrollView, Text, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
Text,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { showSuccessAlert } from "@/utils/alerts";
|
||||
import { useCancelBooking } from "@/hooks/useBookings";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
const CopyButton = ({
|
||||
text,
|
||||
color,
|
||||
size = 16,
|
||||
}: {
|
||||
text: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
}) => (
|
||||
<AppPressable
|
||||
onPress={async () => {
|
||||
if (!text) return;
|
||||
await Clipboard.setStringAsync(text);
|
||||
showSuccessAlert("Copied", "Copied to clipboard");
|
||||
}}
|
||||
hitSlop={8}
|
||||
className="ml-2 justify-center"
|
||||
>
|
||||
<Ionicons name="copy-outline" size={size} color={color || "#A3A3A3"} />
|
||||
</AppPressable>
|
||||
);
|
||||
|
||||
// Format date for iOS Calendar style: "Thursday, 25 Dec 2025"
|
||||
const formatDateCalendarStyle = (dateString: string): string => {
|
||||
if (!dateString) return "";
|
||||
@@ -110,6 +142,19 @@ export function BookingDetailScreen({
|
||||
}: BookingDetailScreenProps): React.JSX.Element {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#f2f2f7",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#A3A3A3",
|
||||
textTertiary: isDark ? "#636366" : "#636366",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
destructive: isDark ? "#FF453A" : "#800020",
|
||||
badge: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
};
|
||||
|
||||
const [participantsExpanded, setParticipantsExpanded] = useState(true);
|
||||
|
||||
@@ -253,9 +298,14 @@ export function BookingDetailScreen({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-[#f2f2f7]">
|
||||
<ActivityIndicator size="large" color="#000000" />
|
||||
<Text className="mt-4 text-base text-gray-500">Loading booking...</Text>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: colors.background }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={colors.text} />
|
||||
<Text className="mt-4 text-base" style={{ color: colors.textSecondary }}>
|
||||
Loading booking...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -263,13 +313,25 @@ export function BookingDetailScreen({
|
||||
if (error || !booking) {
|
||||
const errorMessage = error?.message || "Booking not found";
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-[#f2f2f7] p-5">
|
||||
<View
|
||||
className="flex-1 items-center justify-center p-5"
|
||||
style={{ backgroundColor: colors.background }}
|
||||
>
|
||||
<Ionicons name="alert-circle" size={64} color="#FF3B30" />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-gray-800">
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold" style={{ color: colors.text }}>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
<AppPressable className="mt-6 rounded-lg bg-black px-6 py-3" onPress={() => router.back()}>
|
||||
<Text className="text-base font-semibold text-white">Go Back</Text>
|
||||
<AppPressable
|
||||
className="mt-6 rounded-lg px-6 py-3"
|
||||
style={{ backgroundColor: isDark ? "#FFFFFF" : "#000000" }}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Text
|
||||
className="text-base font-semibold"
|
||||
style={{ color: isDark ? "#000000" : "#FFFFFF" }}
|
||||
>
|
||||
Go Back
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</View>
|
||||
);
|
||||
@@ -310,7 +372,7 @@ export function BookingDetailScreen({
|
||||
if (normalizedStatus === "pending") {
|
||||
return {
|
||||
name: "help-circle" as const,
|
||||
color: "#8E8E93",
|
||||
color: "#A3A3A3",
|
||||
label: "Pending",
|
||||
};
|
||||
}
|
||||
@@ -318,7 +380,7 @@ export function BookingDetailScreen({
|
||||
if (normalizedStatus === "cancelled" || normalizedStatus === "rejected") {
|
||||
return {
|
||||
name: "close-circle-outline" as const,
|
||||
color: "#8E8E93",
|
||||
color: "#A3A3A3",
|
||||
label: null,
|
||||
};
|
||||
}
|
||||
@@ -339,10 +401,12 @@ export function BookingDetailScreen({
|
||||
>
|
||||
{/* Title Section - iOS Calendar Style */}
|
||||
<View className="mb-8">
|
||||
{/* Meeting Title */}
|
||||
{/* Meeting Title */}
|
||||
<Text
|
||||
className="mb-4 text-[26px] font-semibold leading-tight text-black"
|
||||
style={{ letterSpacing: -0.3 }}
|
||||
className="mb-4 text-[26px] font-semibold leading-tight"
|
||||
style={{ letterSpacing: -0.3, color: colors.text }}
|
||||
selectable
|
||||
>
|
||||
{booking.title}
|
||||
</Text>
|
||||
@@ -350,13 +414,25 @@ export function BookingDetailScreen({
|
||||
{/* Event Type Slug and Duration Badge */}
|
||||
{(eventTypeSlug || durationFormatted) && (
|
||||
<View className="mb-3 flex-row items-center">
|
||||
{eventTypeSlug && <Text className="text-[15px] text-[#8E8E93]">{eventTypeSlug}</Text>}
|
||||
{eventTypeSlug && (
|
||||
<Text className="text-[15px]" style={{ color: colors.textSecondary }}>
|
||||
{eventTypeSlug}
|
||||
</Text>
|
||||
)}
|
||||
{eventTypeSlug && durationFormatted && (
|
||||
<Text className="mx-2 text-[15px] text-[#C7C7CC]">•</Text>
|
||||
<Text className="mx-2 text-[15px]" style={{ color: colors.textSecondary }}>
|
||||
•
|
||||
</Text>
|
||||
)}
|
||||
{durationFormatted && (
|
||||
<View className="rounded-full bg-[#E5E5EA] px-2.5 py-1">
|
||||
<Text className="text-[13px] font-medium text-[#636366]">
|
||||
<View
|
||||
className="rounded-full px-2.5 py-1"
|
||||
style={{ backgroundColor: isDark ? "#171717" : colors.badge }}
|
||||
>
|
||||
<Text
|
||||
className="text-[13px] font-medium"
|
||||
style={{ color: isDark ? "#FFFFFF" : colors.textTertiary }}
|
||||
>
|
||||
{durationFormatted}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -365,58 +441,85 @@ export function BookingDetailScreen({
|
||||
)}
|
||||
|
||||
{/* Date */}
|
||||
<Text className="mb-0.5 text-[17px] text-black">{dateFormatted}</Text>
|
||||
<Text className="mb-0.5 text-[17px]" style={{ color: colors.text }}>
|
||||
{dateFormatted}
|
||||
</Text>
|
||||
|
||||
{/* Time */}
|
||||
<Text className="mb-0.5 text-[17px] text-black">{timeFormatted}</Text>
|
||||
<Text className="mb-0.5 text-[17px]" style={{ color: colors.text }}>
|
||||
{timeFormatted}
|
||||
</Text>
|
||||
|
||||
{/* Recurring indicator */}
|
||||
{isRecurring && <Text className="mt-0.5 text-[17px] text-[#800020]">Repeats weekly</Text>}
|
||||
{isRecurring && (
|
||||
<Text className="mt-0.5 text-[17px]" style={{ color: colors.destructive }}>
|
||||
Repeats weekly
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Participants Card - iOS Calendar Style (Expandable) */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<AppPressable
|
||||
className="flex-row items-center justify-between px-4 py-3.5"
|
||||
onPress={() => setParticipantsExpanded(!participantsExpanded)}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Participants</Text>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
Participants
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">{totalParticipants}</Text>
|
||||
<Text className="mr-1 text-[17px]" style={{ color: colors.textSecondary }}>
|
||||
{totalParticipants}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={participantsExpanded ? "chevron-down" : "chevron-forward"}
|
||||
size={18}
|
||||
color="#C7C7CC"
|
||||
color={colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
</AppPressable>
|
||||
|
||||
{/* Participants list - only show when expanded */}
|
||||
{participantsExpanded && (
|
||||
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
|
||||
<View
|
||||
className="px-4 py-2.5"
|
||||
style={{ borderTopWidth: 1, borderTopColor: colors.border }}
|
||||
>
|
||||
{/* Hosts */}
|
||||
{booking.hosts && booking.hosts.length > 0
|
||||
? booking.hosts.map((host, index) => (
|
||||
<View
|
||||
key={host.email || `host-${index}`}
|
||||
className={`flex-row items-center py-2 ${
|
||||
index > 0 ? "border-t border-[#E5E5EA]" : ""
|
||||
}`}
|
||||
className="flex-row items-center py-2"
|
||||
style={index > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}}
|
||||
>
|
||||
<Ionicons name="star" size={18} color="#FFD60A" />
|
||||
<Text className="ml-2.5 flex-1 text-[15px] text-black" numberOfLines={1}>
|
||||
<Text
|
||||
className="ml-2.5 flex-1 text-[15px]"
|
||||
style={{ color: colors.text }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{host.name || host.email || "Host"}
|
||||
<Text style={{ color: colors.textSecondary }}> (Organizer)</Text>
|
||||
</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Organizer</Text>
|
||||
<CopyButton text={host.name || host.email || "Host"} />
|
||||
</View>
|
||||
))
|
||||
: booking.user && (
|
||||
<View className="flex-row items-center py-2">
|
||||
<Ionicons name="star" size={18} color="#FFD60A" />
|
||||
<Text className="ml-2.5 flex-1 text-[15px] text-black" numberOfLines={1}>
|
||||
<Text
|
||||
className="ml-2.5 flex-1 text-[15px]"
|
||||
style={{ color: colors.text }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{booking.user.name || booking.user.email}
|
||||
<Text style={{ color: colors.textSecondary }}> (Organizer)</Text>
|
||||
</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Organizer</Text>
|
||||
<CopyButton text={booking.user.name || booking.user.email} />
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -426,21 +529,25 @@ export function BookingDetailScreen({
|
||||
return (
|
||||
<View
|
||||
key={attendee.email}
|
||||
className={`flex-row items-center py-2 ${
|
||||
className="flex-row items-center py-2"
|
||||
style={
|
||||
index > 0 || booking.hosts?.length || booking.user
|
||||
? "border-t border-[#E5E5EA]"
|
||||
: ""
|
||||
}`}
|
||||
? { borderTopWidth: 1, borderTopColor: colors.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Ionicons name={statusIcon.name} size={20} color={statusIcon.color} />
|
||||
<Text className="ml-2.5 flex-1 text-[15px] text-black" numberOfLines={1}>
|
||||
<Text
|
||||
className="ml-2.5 flex-1 text-[15px]"
|
||||
style={{ color: colors.text }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{attendee.name || attendee.email}
|
||||
{statusIcon.label ? (
|
||||
<Text style={{ color: statusIcon.color }}> ({statusIcon.label})</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
{statusIcon.label && (
|
||||
<Text className="text-[13px]" style={{ color: statusIcon.color }}>
|
||||
{statusIcon.label}
|
||||
</Text>
|
||||
)}
|
||||
<CopyButton text={attendee.name || attendee.email} />
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
@@ -449,17 +556,23 @@ export function BookingDetailScreen({
|
||||
{booking.guests?.map((guestEmail, index) => (
|
||||
<View
|
||||
key={guestEmail}
|
||||
className={`flex-row items-center py-2 ${
|
||||
className="flex-row items-center py-2"
|
||||
style={
|
||||
index > 0 || booking.attendees?.length || booking.hosts?.length || booking.user
|
||||
? "border-t border-[#E5E5EA]"
|
||||
: ""
|
||||
}`}
|
||||
? { borderTopWidth: 1, borderTopColor: colors.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Ionicons name="person-outline" size={20} color="#8E8E93" />
|
||||
<Text className="ml-2.5 flex-1 text-[15px] text-[#8E8E93]" numberOfLines={1}>
|
||||
<Ionicons name="person-outline" size={20} color={colors.textSecondary} />
|
||||
<Text
|
||||
className="ml-2.5 flex-1 text-[15px]"
|
||||
style={{ color: colors.textSecondary }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{guestEmail}
|
||||
<Text style={{ color: colors.textSecondary }}> (Guest)</Text>
|
||||
</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Guest</Text>
|
||||
<CopyButton text={guestEmail} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
@@ -503,9 +616,15 @@ export function BookingDetailScreen({
|
||||
if (displayableEntries.length === 0) return null;
|
||||
|
||||
return (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View className="px-4 py-3.5">
|
||||
<Text className="mb-2.5 text-[13px] font-medium uppercase tracking-wide text-[#8E8E93]">
|
||||
<Text
|
||||
className="mb-2.5 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Booking Details
|
||||
</Text>
|
||||
{displayableEntries.map(([key, value], index) => {
|
||||
@@ -530,10 +649,18 @@ export function BookingDetailScreen({
|
||||
return (
|
||||
<View
|
||||
key={key}
|
||||
className={`py-2 ${index > 0 ? "border-t border-[#E5E5EA]" : ""}`}
|
||||
className="py-2"
|
||||
style={index > 0 ? { borderTopWidth: 1, borderTopColor: colors.border } : {}}
|
||||
>
|
||||
<Text className="mb-0.5 text-[13px] text-[#8E8E93]">{displayKey}</Text>
|
||||
<Text className="text-[17px] text-black">{displayValue}</Text>
|
||||
<Text className="mb-0.5 text-[13px]" style={{ color: colors.textSecondary }}>
|
||||
{displayKey}
|
||||
</Text>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-[17px] flex-1" style={{ color: colors.text }}>
|
||||
{displayValue}
|
||||
</Text>
|
||||
<CopyButton text={displayValue} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
@@ -544,12 +671,23 @@ export function BookingDetailScreen({
|
||||
|
||||
{/* Description Card (if available) */}
|
||||
{booking.description && (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View className="px-4 py-3.5">
|
||||
<Text className="mb-1.5 text-[13px] font-medium uppercase tracking-wide text-[#8E8E93]">
|
||||
<Text
|
||||
className="mb-1.5 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Notes
|
||||
</Text>
|
||||
<Text className="text-[17px] leading-6 text-black">{booking.description}</Text>
|
||||
<View className="flex-row items-start justify-between">
|
||||
<Text className="text-[17px] leading-6 flex-1 mr-2" style={{ color: colors.text }}>
|
||||
{booking.description}
|
||||
</Text>
|
||||
<CopyButton text={booking.description} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
@@ -559,9 +697,15 @@ export function BookingDetailScreen({
|
||||
booking.rescheduledToUid ||
|
||||
booking.reschedulingReason ||
|
||||
booking.fromReschedule) && (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View className="px-4 py-3.5">
|
||||
<Text className="mb-2.5 text-[13px] font-medium uppercase tracking-wide text-[#8E8E93]">
|
||||
<Text
|
||||
className="mb-2.5 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Rescheduling Info
|
||||
</Text>
|
||||
|
||||
@@ -578,7 +722,9 @@ export function BookingDetailScreen({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Rescheduled from</Text>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
Rescheduled from
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[15px] text-[#007AFF]">View original</Text>
|
||||
<Ionicons name="chevron-forward" size={16} color="#007AFF" />
|
||||
@@ -588,7 +734,8 @@ export function BookingDetailScreen({
|
||||
|
||||
{booking.rescheduledToUid && (
|
||||
<AppPressable
|
||||
className="flex-row items-center justify-between border-t border-[#E5E5EA] py-2"
|
||||
className="flex-row items-center justify-between py-2"
|
||||
style={{ borderTopWidth: 1, borderTopColor: colors.border }}
|
||||
onPress={() => {
|
||||
const uid = booking.rescheduledToUid;
|
||||
if (uid) {
|
||||
@@ -599,7 +746,9 @@ export function BookingDetailScreen({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Text className="text-[17px] text-black">Rescheduled to</Text>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
Rescheduled to
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[15px] text-[#007AFF]">View new</Text>
|
||||
<Ionicons name="chevron-forward" size={16} color="#007AFF" />
|
||||
@@ -609,14 +758,22 @@ export function BookingDetailScreen({
|
||||
|
||||
{booking.reschedulingReason && (
|
||||
<View
|
||||
className={`py-2 ${
|
||||
className="py-2"
|
||||
style={
|
||||
booking.rescheduledFromUid || booking.rescheduledToUid || booking.fromReschedule
|
||||
? "border-t border-[#E5E5EA]"
|
||||
: ""
|
||||
}`}
|
||||
? { borderTopWidth: 1, borderTopColor: colors.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Text className="mb-0.5 text-[13px] text-[#8E8E93]">Reason</Text>
|
||||
<Text className="text-[17px] text-black">{booking.reschedulingReason}</Text>
|
||||
<Text className="mb-0.5 text-[13px]" style={{ color: colors.textSecondary }}>
|
||||
Reason
|
||||
</Text>
|
||||
<View className="flex-row items-start justify-between">
|
||||
<Text className="text-[17px] flex-1" style={{ color: colors.text }}>
|
||||
{booking.reschedulingReason}
|
||||
</Text>
|
||||
<CopyButton text={booking.reschedulingReason} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -626,7 +783,10 @@ export function BookingDetailScreen({
|
||||
{/* Cancellation Info Card (if cancelled) */}
|
||||
{normalizedStatus === "cancelled" &&
|
||||
(booking.cancellationReason || booking.cancelledByEmail || booking.absentHost) && (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View className="px-4 py-3.5">
|
||||
<Text className="mb-2.5 text-[13px] font-medium uppercase tracking-wide text-[#FF3B30]">
|
||||
Cancellation Details
|
||||
@@ -634,32 +794,52 @@ export function BookingDetailScreen({
|
||||
|
||||
{booking.cancellationReason && (
|
||||
<View className="py-2">
|
||||
<Text className="mb-0.5 text-[13px] text-[#8E8E93]">Reason</Text>
|
||||
<Text className="text-[17px] text-black">{booking.cancellationReason}</Text>
|
||||
<Text className="mb-0.5 text-[13px]" style={{ color: colors.textSecondary }}>
|
||||
Reason
|
||||
</Text>
|
||||
<View className="flex-row items-start justify-between">
|
||||
<Text className="text-[17px] flex-1" style={{ color: colors.text }}>
|
||||
{booking.cancellationReason}
|
||||
</Text>
|
||||
<CopyButton text={booking.cancellationReason} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{booking.cancelledByEmail && (
|
||||
<View
|
||||
className={`py-2 ${
|
||||
booking.cancellationReason ? "border-t border-[#E5E5EA]" : ""
|
||||
}`}
|
||||
className="py-2"
|
||||
style={
|
||||
booking.cancellationReason
|
||||
? { borderTopWidth: 1, borderTopColor: colors.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Text className="mb-0.5 text-[13px] text-[#8E8E93]">Cancelled by</Text>
|
||||
<Text className="text-[17px] text-black">{booking.cancelledByEmail}</Text>
|
||||
<Text className="mb-0.5 text-[13px]" style={{ color: colors.textSecondary }}>
|
||||
Cancelled by
|
||||
</Text>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-[17px] flex-1" style={{ color: colors.text }}>
|
||||
{booking.cancelledByEmail}
|
||||
</Text>
|
||||
<CopyButton text={booking.cancelledByEmail} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{booking.absentHost && (
|
||||
<View
|
||||
className={`flex-row items-center py-2 ${
|
||||
className="flex-row items-center py-2"
|
||||
style={
|
||||
booking.cancellationReason || booking.cancelledByEmail
|
||||
? "border-t border-[#E5E5EA]"
|
||||
: ""
|
||||
}`}
|
||||
? { borderTopWidth: 1, borderTopColor: colors.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Ionicons name="warning" size={18} color="#FF9500" />
|
||||
<Text className="ml-2 text-[17px] text-black">Host was absent</Text>
|
||||
<Text className="ml-2 text-[17px]" style={{ color: colors.text }}>
|
||||
Host was absent
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
@@ -670,9 +850,14 @@ export function BookingDetailScreen({
|
||||
{/* Cancelling overlay */}
|
||||
{isCancelling && (
|
||||
<View className="absolute inset-0 items-center justify-center bg-black/50">
|
||||
<View className="rounded-2xl bg-white px-8 py-6">
|
||||
<ActivityIndicator size="large" color="#000" />
|
||||
<Text className="mt-3 text-base font-medium text-gray-700">Cancelling booking...</Text>
|
||||
<View
|
||||
className="rounded-2xl px-8 py-6"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={colors.text} />
|
||||
<Text className="mt-3 text-base font-medium" style={{ color: colors.textSecondary }}>
|
||||
Cancelling booking...
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import { BookingActionsModal } from "@/components/BookingActionsModal";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
import { HeaderButtonWrapper } from "@/components/HeaderButtonWrapper";
|
||||
@@ -42,6 +44,29 @@ import type { Booking } from "@/services/calcom";
|
||||
import { showErrorAlert, showInfoAlert, showSuccessAlert } from "@/utils/alerts";
|
||||
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
const CopyButton = ({
|
||||
text,
|
||||
color,
|
||||
size = 16,
|
||||
}: {
|
||||
text: string;
|
||||
color?: string;
|
||||
size?: number;
|
||||
}) => (
|
||||
<AppPressable
|
||||
onPress={async () => {
|
||||
if (!text) return;
|
||||
await Clipboard.setStringAsync(text);
|
||||
showSuccessAlert("Copied", "Copied to clipboard");
|
||||
}}
|
||||
hitSlop={8}
|
||||
className="ml-2 justify-center"
|
||||
>
|
||||
<Ionicons name="copy-outline" size={size} color={color || "#A3A3A3"} />
|
||||
</AppPressable>
|
||||
);
|
||||
|
||||
// Empty actions result for when no booking is loaded
|
||||
const EMPTY_ACTIONS: BookingActionsResult = {
|
||||
@@ -174,6 +199,14 @@ export function BookingDetailScreen({
|
||||
const router = useRouter();
|
||||
const { userInfo } = useAuth();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
|
||||
// Background for the main screen (grouped list style: gray in light, black in dark)
|
||||
const screenBackground = isDark ? theme.background : theme.backgroundMuted;
|
||||
// Background for cards/items (white in light, dark gray in dark)
|
||||
const cardBackground = isDark ? theme.backgroundSecondary : theme.background;
|
||||
|
||||
const [showActionsModal, setShowActionsModal] = useState(false);
|
||||
const [showCancelDialog, setShowCancelDialog] = useState(false);
|
||||
@@ -456,9 +489,14 @@ export function BookingDetailScreen({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-[#f2f2f7]">
|
||||
<ActivityIndicator size="large" color="#000000" />
|
||||
<Text className="mt-4 text-base text-gray-500">Loading booking...</Text>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: screenBackground }}
|
||||
>
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
<Text className="mt-4 text-base" style={{ color: theme.textSecondary }}>
|
||||
Loading booking...
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -466,13 +504,25 @@ export function BookingDetailScreen({
|
||||
if (error || !booking) {
|
||||
const errorMessage = error?.message || "Booking not found";
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-[#f2f2f7] p-5">
|
||||
<Ionicons name="alert-circle" size={64} color="#FF3B30" />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-gray-800">
|
||||
<View
|
||||
className="flex-1 items-center justify-center p-5"
|
||||
style={{ backgroundColor: screenBackground }}
|
||||
>
|
||||
<Ionicons name="alert-circle" size={64} color={theme.error} />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold" style={{ color: theme.text }}>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
<AppPressable className="mt-6 rounded-lg bg-black px-6 py-3" onPress={() => router.back()}>
|
||||
<Text className="text-base font-semibold text-white">Go Back</Text>
|
||||
<AppPressable
|
||||
className="mt-6 rounded-lg px-6 py-3"
|
||||
style={{ backgroundColor: isDark ? "#FFFFFF" : "#000000" }}
|
||||
onPress={() => router.back()}
|
||||
>
|
||||
<Text
|
||||
className="text-base font-semibold"
|
||||
style={{ color: isDark ? "#000000" : "#FFFFFF" }}
|
||||
>
|
||||
Go Back
|
||||
</Text>
|
||||
</AppPressable>
|
||||
</View>
|
||||
);
|
||||
@@ -514,7 +564,7 @@ export function BookingDetailScreen({
|
||||
if (normalizedStatus === "pending") {
|
||||
return {
|
||||
name: "help-circle" as const,
|
||||
color: "#8E8E93",
|
||||
color: "#A3A3A3",
|
||||
label: "Pending",
|
||||
};
|
||||
}
|
||||
@@ -522,7 +572,7 @@ export function BookingDetailScreen({
|
||||
if (normalizedStatus === "cancelled" || normalizedStatus === "rejected") {
|
||||
return {
|
||||
name: "close-circle-outline" as const,
|
||||
color: "#8E8E93",
|
||||
color: "#A3A3A3",
|
||||
label: null,
|
||||
};
|
||||
}
|
||||
@@ -539,18 +589,17 @@ export function BookingDetailScreen({
|
||||
headerRight: () => (
|
||||
<HeaderButtonWrapper side="right">
|
||||
<View className="flex-row items-center">
|
||||
{meetingUrl && (
|
||||
<Pressable
|
||||
className="mr-2 h-10 w-10 items-center justify-center rounded-full"
|
||||
onPress={handleJoinMeeting}
|
||||
>
|
||||
<Ionicons name="videocam" size={24} color="#000" />
|
||||
</Pressable>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Pressable className="h-10 w-10 items-center justify-center rounded-full">
|
||||
<Ionicons name="ellipsis-horizontal" size={24} color="#000" />
|
||||
<Pressable
|
||||
className="mr-2 h-9 w-9 items-center justify-center rounded-full"
|
||||
style={{
|
||||
backgroundColor: cardBackground,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.border,
|
||||
}}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={20} color={theme.text} />
|
||||
</Pressable>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
@@ -569,7 +618,13 @@ export function BookingDetailScreen({
|
||||
<Ionicons
|
||||
name={action.icon}
|
||||
size={18}
|
||||
color={action.variant === "destructive" ? "#800020" : "#374151"}
|
||||
color={
|
||||
action.variant === "destructive"
|
||||
? theme.destructive
|
||||
: isDark
|
||||
? "#FFFFFF"
|
||||
: "#374151"
|
||||
}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<UIText
|
||||
@@ -582,13 +637,28 @@ export function BookingDetailScreen({
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{meetingUrl && (
|
||||
<Pressable
|
||||
className="h-9 items-center justify-center rounded-full px-4"
|
||||
style={{ backgroundColor: theme.text }}
|
||||
onPress={handleJoinMeeting}
|
||||
>
|
||||
<Text
|
||||
className="text-[14px] font-semibold"
|
||||
style={{ color: theme.background }}
|
||||
>
|
||||
Join
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
</HeaderButtonWrapper>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<View className="flex-1 bg-[#f2f2f7]">
|
||||
<View className="flex-1" style={{ backgroundColor: screenBackground }}>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 100 }}
|
||||
@@ -597,9 +667,11 @@ export function BookingDetailScreen({
|
||||
>
|
||||
{/* Title Section - iOS Calendar Style */}
|
||||
<View className="mb-8">
|
||||
{/* Meeting Title */}
|
||||
<Text
|
||||
className="mb-4 text-[26px] font-semibold leading-tight text-black"
|
||||
style={{ letterSpacing: -0.3 }}
|
||||
className="mb-4 text-[26px] font-semibold leading-tight"
|
||||
style={{ letterSpacing: -0.3, color: theme.text }}
|
||||
selectable
|
||||
>
|
||||
{booking.title}
|
||||
</Text>
|
||||
@@ -607,14 +679,28 @@ export function BookingDetailScreen({
|
||||
{eventTypeSlug || durationFormatted ? (
|
||||
<View className="mb-3 flex-row items-center">
|
||||
{eventTypeSlug ? (
|
||||
<Text className="text-[15px] text-[#8E8E93]">{eventTypeSlug}</Text>
|
||||
<Text className="text-[15px]" style={{ color: theme.textSecondary }} selectable>
|
||||
{eventTypeSlug}
|
||||
</Text>
|
||||
) : null}
|
||||
{eventTypeSlug && durationFormatted ? (
|
||||
<Text className="mx-2 text-[15px] text-[#C7C7CC]">•</Text>
|
||||
<Text className="mx-2 text-[15px]" style={{ color: theme.textSecondary }}>
|
||||
•
|
||||
</Text>
|
||||
) : null}
|
||||
{durationFormatted ? (
|
||||
<View className="rounded-full bg-[#E5E5EA] px-2.5 py-1">
|
||||
<Text className="text-[13px] font-medium text-[#636366]">
|
||||
<View
|
||||
className="rounded-full px-2.5 py-1"
|
||||
style={{
|
||||
backgroundColor: isDark
|
||||
? theme.backgroundSecondary
|
||||
: theme.backgroundEmphasis,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
className="text-[13px] font-medium"
|
||||
style={{ color: isDark ? "#FFFFFF" : theme.text }}
|
||||
>
|
||||
{durationFormatted}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -622,55 +708,80 @@ export function BookingDetailScreen({
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<Text className="mb-0.5 text-[17px] text-black">{dateFormatted}</Text>
|
||||
<Text className="mb-0.5 text-[17px] text-black">{timeFormatted}</Text>
|
||||
<Text className="mb-0.5 text-[17px]" style={{ color: theme.text }} selectable>
|
||||
{dateFormatted}
|
||||
</Text>
|
||||
<Text className="mb-0.5 text-[17px]" style={{ color: theme.text }} selectable>
|
||||
{timeFormatted}
|
||||
</Text>
|
||||
|
||||
{isRecurring ? (
|
||||
<Text className="mt-0.5 text-[17px] text-[#800020]">Repeats weekly</Text>
|
||||
<Text className="mt-0.5 text-[17px]" style={{ color: theme.destructive }}>
|
||||
Repeats weekly
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* Participants Card - iOS Calendar Style (Expandable) */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: cardBackground }}
|
||||
>
|
||||
<AppPressable onPress={() => setParticipantsExpanded(!participantsExpanded)}>
|
||||
<View className="flex-row items-center justify-between px-4 py-3.5">
|
||||
<Text className="text-[17px] text-black">Participants</Text>
|
||||
<Text className="text-[17px]" style={{ color: theme.text }}>
|
||||
Participants
|
||||
</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Text className="mr-1 text-[17px] text-[#8E8E93]">{totalParticipants}</Text>
|
||||
<Text className="mr-1 text-[17px]" style={{ color: theme.textSecondary }}>
|
||||
{totalParticipants}
|
||||
</Text>
|
||||
<Ionicons
|
||||
name={participantsExpanded ? "chevron-down" : "chevron-forward"}
|
||||
size={18}
|
||||
color="#C7C7CC"
|
||||
color={theme.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</AppPressable>
|
||||
|
||||
{participantsExpanded ? (
|
||||
<View className="border-t border-[#E5E5EA] px-4 py-2.5">
|
||||
<View
|
||||
className="px-4 py-2.5"
|
||||
style={{ borderTopWidth: 1, borderTopColor: theme.border }}
|
||||
>
|
||||
{/* Hosts */}
|
||||
{booking.hosts && booking.hosts.length > 0 ? (
|
||||
booking.hosts.map((host, index) => (
|
||||
<View
|
||||
key={host.email || `host-${index}`}
|
||||
className={`flex-row items-center py-2 ${
|
||||
index > 0 ? "border-t border-[#E5E5EA]" : ""
|
||||
}`}
|
||||
className="flex-row items-center py-2"
|
||||
style={index > 0 ? { borderTopWidth: 1, borderTopColor: theme.border } : {}}
|
||||
>
|
||||
<Ionicons name="star" size={18} color="#FFD60A" />
|
||||
<Text className="ml-2.5 flex-1 text-[15px] text-black" numberOfLines={1}>
|
||||
<Text
|
||||
className="ml-2.5 flex-1 text-[15px]"
|
||||
style={{ color: theme.text }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{host.name || host.email || "Host"}
|
||||
<Text style={{ color: theme.textSecondary }}> (Organizer)</Text>
|
||||
</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Organizer</Text>
|
||||
<CopyButton text={host.name || host.email || "Host"} />
|
||||
</View>
|
||||
))
|
||||
) : booking.user ? (
|
||||
<View className="flex-row items-center py-2">
|
||||
<Ionicons name="star" size={18} color="#FFD60A" />
|
||||
<Text className="ml-2.5 flex-1 text-[15px] text-black" numberOfLines={1}>
|
||||
<Text
|
||||
className="ml-2.5 flex-1 text-[15px]"
|
||||
style={{ color: theme.text }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{booking.user.name || booking.user.email}
|
||||
<Text style={{ color: theme.textSecondary }}> (Organizer)</Text>
|
||||
</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Organizer</Text>
|
||||
<CopyButton text={booking.user.name || booking.user.email} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
@@ -680,60 +791,300 @@ export function BookingDetailScreen({
|
||||
return (
|
||||
<View
|
||||
key={attendee.email}
|
||||
className={`flex-row items-center py-2 ${
|
||||
className="flex-row items-center py-2"
|
||||
style={
|
||||
index > 0 || booking.hosts?.length || booking.user
|
||||
? "border-t border-[#E5E5EA]"
|
||||
: ""
|
||||
}`}
|
||||
? { borderTopWidth: 1, borderTopColor: theme.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Ionicons name={statusIcon.name} size={20} color={statusIcon.color} />
|
||||
<Text className="ml-2.5 flex-1 text-[15px] text-black" numberOfLines={1}>
|
||||
<Text
|
||||
className="ml-2.5 flex-1 text-[15px]"
|
||||
style={{ color: theme.text }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{attendee.name || attendee.email}
|
||||
{statusIcon.label ? (
|
||||
<Text style={{ color: statusIcon.color }}> ({statusIcon.label})</Text>
|
||||
) : null}
|
||||
</Text>
|
||||
{statusIcon.label ? (
|
||||
<Text className="text-[13px]" style={{ color: statusIcon.color }}>
|
||||
{statusIcon.label}
|
||||
</Text>
|
||||
) : null}
|
||||
<CopyButton text={attendee.name || attendee.email} />
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Guests */}
|
||||
{/* Guests */}
|
||||
{(booking as { guests?: string[] }).guests?.map((guestEmail, index) => (
|
||||
<View
|
||||
key={guestEmail}
|
||||
className={`flex-row items-center py-2 ${
|
||||
className="flex-row items-center py-2"
|
||||
style={
|
||||
index > 0 ||
|
||||
booking.attendees?.length ||
|
||||
booking.hosts?.length ||
|
||||
booking.user
|
||||
? "border-t border-[#E5E5EA]"
|
||||
: ""
|
||||
}`}
|
||||
? { borderTopWidth: 1, borderTopColor: theme.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Ionicons name="person-outline" size={20} color="#8E8E93" />
|
||||
<Text className="ml-2.5 flex-1 text-[15px] text-[#8E8E93]" numberOfLines={1}>
|
||||
<Ionicons name="person-outline" size={20} color={theme.textSecondary} />
|
||||
<Text
|
||||
className="ml-2.5 flex-1 text-[15px]"
|
||||
style={{ color: theme.textSecondary }}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{guestEmail}
|
||||
<Text style={{ color: theme.textSecondary }}> (Guest)</Text>
|
||||
</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Guest</Text>
|
||||
<CopyButton text={guestEmail} />
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* Custom Fields Card (if available) */}
|
||||
{(() => {
|
||||
if (!booking.bookingFieldsResponses) return null;
|
||||
|
||||
const excludedKeys = [
|
||||
"location",
|
||||
"guests",
|
||||
"rescheduledReason",
|
||||
"rescheduleReason",
|
||||
"email",
|
||||
"name",
|
||||
"notes",
|
||||
"description",
|
||||
"additionalNotes",
|
||||
];
|
||||
|
||||
const displayableEntries = Object.entries(booking.bookingFieldsResponses).filter(
|
||||
([key, value]) => {
|
||||
if (excludedKeys.includes(key)) return false;
|
||||
|
||||
if (value === null || value === undefined || value === "") return false;
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) return false;
|
||||
|
||||
if (typeof value === "object" && !Array.isArray(value) && value !== null) {
|
||||
const obj = value as Record<string, unknown>;
|
||||
if ("value" in obj && "optionValue" in obj) return false;
|
||||
if (Object.keys(obj).length === 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
if (displayableEntries.length === 0) return null;
|
||||
|
||||
return (
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: cardBackground }}
|
||||
>
|
||||
<View className="px-4 py-3.5">
|
||||
<Text
|
||||
className="mb-2.5 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: theme.textSecondary }}
|
||||
>
|
||||
Booking Details
|
||||
</Text>
|
||||
{displayableEntries.map(([key, value], index) => {
|
||||
let displayValue: string;
|
||||
if (typeof value === "string") {
|
||||
displayValue = value;
|
||||
} else if (Array.isArray(value)) {
|
||||
displayValue = value.join(", ");
|
||||
} else if (typeof value === "boolean") {
|
||||
displayValue = value ? "Yes" : "No";
|
||||
} else if (typeof value === "number") {
|
||||
displayValue = String(value);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayKey = key
|
||||
.replace(/([A-Z])/g, " $1")
|
||||
.replace(/^./, (str) => str.toUpperCase())
|
||||
.trim();
|
||||
|
||||
return (
|
||||
<View
|
||||
key={key}
|
||||
className="py-2"
|
||||
style={index > 0 ? { borderTopWidth: 1, borderTopColor: theme.border } : {}}
|
||||
>
|
||||
<Text className="mb-0.5 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
{displayKey}
|
||||
</Text>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-[17px] flex-1" style={{ color: theme.text }}>
|
||||
{displayValue}
|
||||
</Text>
|
||||
<CopyButton text={displayValue} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Notes Card (if available) */}
|
||||
{booking.description ? (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: cardBackground }}
|
||||
>
|
||||
<View className="px-4 py-3.5">
|
||||
<Text className="mb-1.5 text-[13px] font-medium uppercase tracking-wide text-[#8E8E93]">
|
||||
<Text
|
||||
className="mb-1.5 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: theme.textSecondary }}
|
||||
>
|
||||
Notes
|
||||
</Text>
|
||||
<Text className="text-[17px] leading-6 text-black">{booking.description}</Text>
|
||||
<View className="flex-row items-start justify-between">
|
||||
<Text className="text-[17px] leading-6 flex-1 mr-2" style={{ color: theme.text }}>
|
||||
{booking.description}
|
||||
</Text>
|
||||
<CopyButton text={booking.description} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{/* Rescheduling Info Card (if applicable) */}
|
||||
{(booking.rescheduledFromUid ||
|
||||
booking.rescheduledToUid ||
|
||||
booking.reschedulingReason ||
|
||||
booking.fromReschedule) && (
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: cardBackground }}
|
||||
>
|
||||
<View className="px-4 py-3.5">
|
||||
<Text
|
||||
className="mb-2.5 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: theme.textSecondary }}
|
||||
>
|
||||
Rescheduling Info
|
||||
</Text>
|
||||
|
||||
{(booking.rescheduledFromUid || booking.fromReschedule) && (
|
||||
<View className="py-2">
|
||||
<Text className="text-[17px]" style={{ color: theme.text }}>
|
||||
Rescheduled from previous booking
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{booking.rescheduledToUid && (
|
||||
<View
|
||||
className="py-2"
|
||||
style={{ borderTopWidth: 1, borderTopColor: theme.border }}
|
||||
>
|
||||
<Text className="text-[17px]" style={{ color: theme.text }}>
|
||||
Rescheduled to new booking
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{booking.reschedulingReason && (
|
||||
<View
|
||||
className="py-2"
|
||||
style={
|
||||
booking.rescheduledFromUid ||
|
||||
booking.rescheduledToUid ||
|
||||
booking.fromReschedule
|
||||
? { borderTopWidth: 1, borderTopColor: theme.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Text className="mb-0.5 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
Reason
|
||||
</Text>
|
||||
<View className="flex-row items-start justify-between">
|
||||
<Text className="text-[17px] flex-1" style={{ color: theme.text }}>
|
||||
{booking.reschedulingReason}
|
||||
</Text>
|
||||
<CopyButton text={booking.reschedulingReason} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Cancellation Info Card (if cancelled) */}
|
||||
{normalizedStatus === "cancelled" &&
|
||||
(booking.cancellationReason || booking.cancelledByEmail || booking.absentHost) && (
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: cardBackground }}
|
||||
>
|
||||
<View className="px-4 py-3.5">
|
||||
<Text className="mb-2.5 text-[13px] font-medium uppercase tracking-wide text-[#FF3B30]">
|
||||
Cancellation Details
|
||||
</Text>
|
||||
|
||||
{booking.cancellationReason && (
|
||||
<View className="py-2">
|
||||
<Text className="mb-0.5 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
Reason
|
||||
</Text>
|
||||
<View className="flex-row items-start justify-between">
|
||||
<Text className="text-[17px] flex-1" style={{ color: theme.text }}>
|
||||
{booking.cancellationReason}
|
||||
</Text>
|
||||
<CopyButton text={booking.cancellationReason} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{booking.cancelledByEmail && (
|
||||
<View
|
||||
className="py-2"
|
||||
style={
|
||||
booking.cancellationReason
|
||||
? { borderTopWidth: 1, borderTopColor: theme.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Text className="mb-0.5 text-[13px]" style={{ color: theme.textSecondary }}>
|
||||
Cancelled by
|
||||
</Text>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-[17px] flex-1" style={{ color: theme.text }}>
|
||||
{booking.cancelledByEmail}
|
||||
</Text>
|
||||
<CopyButton text={booking.cancelledByEmail} />
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{booking.absentHost && (
|
||||
<View
|
||||
className="flex-row items-center py-2"
|
||||
style={
|
||||
booking.cancellationReason || booking.cancelledByEmail
|
||||
? { borderTopWidth: 1, borderTopColor: theme.border }
|
||||
: {}
|
||||
}
|
||||
>
|
||||
<Ionicons name="warning" size={18} color="#FF9500" />
|
||||
<Text className="ml-2 text-[17px]" style={{ color: theme.text }}>
|
||||
Host was absent
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
|
||||
{/* Booking Actions Modal */}
|
||||
@@ -757,9 +1108,9 @@ export function BookingDetailScreen({
|
||||
{/* Cancelling overlay */}
|
||||
{isCancelling ? (
|
||||
<View className="absolute inset-0 items-center justify-center bg-black/50">
|
||||
<View className="rounded-2xl bg-white px-8 py-6">
|
||||
<ActivityIndicator size="large" color="#000" />
|
||||
<Text className="mt-3 text-base font-medium text-gray-700">
|
||||
<View className="rounded-2xl px-8 py-6" style={{ backgroundColor: cardBackground }}>
|
||||
<ActivityIndicator size="large" color={theme.text} />
|
||||
<Text className="mt-3 text-base font-medium" style={{ color: theme.textSecondary }}>
|
||||
Cancelling booking...
|
||||
</Text>
|
||||
</View>
|
||||
@@ -779,8 +1130,8 @@ export function BookingDetailScreen({
|
||||
<View className="p-6">
|
||||
<View className="flex-row">
|
||||
{/* Danger icon */}
|
||||
<View className="mr-3 self-start rounded-full bg-red-50 p-2">
|
||||
<Ionicons name="alert-circle" size={20} color="#800000" />
|
||||
<View className="mr-3 self-start rounded-full bg-red-50 p-2 dark:bg-red-900/30">
|
||||
<Ionicons name="alert-circle" size={20} color={theme.destructive} />
|
||||
</View>
|
||||
|
||||
{/* Title and description */}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { DatePicker, Host } from "@expo/ui/swift-ui";
|
||||
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, ScrollView, Switch, Text, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { useUpdateSchedule } from "@/hooks/useSchedules";
|
||||
import type { Schedule } from "@/services/calcom";
|
||||
import type { ScheduleAvailability } from "@/services/types";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
@@ -167,7 +168,14 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const backgroundColor = transparentBackground
|
||||
? "transparent"
|
||||
: isDark
|
||||
? theme.background
|
||||
: theme.backgroundMuted;
|
||||
|
||||
// Use mutation hook for cache-synchronized updates
|
||||
const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule();
|
||||
@@ -304,15 +312,16 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-[#8E8E93]">No schedule data</Text>
|
||||
<View className="flex-1 items-center justify-center" style={{ backgroundColor }}>
|
||||
<Text className="text-[#A3A3A3]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className={`flex-1 ${backgroundStyle}`}
|
||||
className="flex-1"
|
||||
style={{ backgroundColor }}
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: insets.bottom + 16,
|
||||
@@ -322,32 +331,49 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
{/* Enable/Disable Toggle */}
|
||||
<View
|
||||
className={`mb-4 flex-row items-center justify-between rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
<Text className="text-[17px] font-medium text-black">Available on {dayName}</Text>
|
||||
<Text className="text-[17px] font-medium text-black dark:text-white">
|
||||
Available on {dayName}
|
||||
</Text>
|
||||
<Switch
|
||||
value={isEnabled}
|
||||
onValueChange={handleToggle}
|
||||
trackColor={{ false: "#E5E5EA", true: "#000000" }}
|
||||
thumbColor="#fff"
|
||||
trackColor={{
|
||||
false: isDark ? theme.backgroundEmphasis : "#E5E5EA",
|
||||
true: isDark ? "#34C759" : "#000000",
|
||||
}}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Time Slots */}
|
||||
{isEnabled && (
|
||||
<>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#8E8E93]">Time Slots</Text>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#A3A3A3]">Time Slots</Text>
|
||||
|
||||
{slots.map((slot, index) => (
|
||||
<View
|
||||
key={`slot-${slot.startTime}-${slot.endTime}-${index}`}
|
||||
className={`mb-3 rounded-xl px-4 py-3 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-[15px] font-medium text-[#8E8E93]">Slot {index + 1}</Text>
|
||||
<Text className="text-[15px] font-medium text-[#A3A3A3]">Slot {index + 1}</Text>
|
||||
{slots.length > 1 && (
|
||||
<AppPressable onPress={() => handleRemoveSlot(index)} className="p-1">
|
||||
<Ionicons name="trash-outline" size={20} color="#FF3B30" />
|
||||
@@ -357,7 +383,7 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
|
||||
<View className="mt-3 flex-row items-center">
|
||||
<View className="flex-1">
|
||||
<Text className="mb-1 text-[13px] text-[#8E8E93]">Start</Text>
|
||||
<Text className="mb-1 text-[13px] text-[#A3A3A3]">Start</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Host matchContents>
|
||||
<DatePicker
|
||||
@@ -369,10 +395,10 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<Text className="mx-3 text-[17px] text-[#8E8E93]">–</Text>
|
||||
<Text className="mx-3 text-[17px] text-[#A3A3A3]">–</Text>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className="mb-1 text-[13px] text-[#8E8E93]">End</Text>
|
||||
<Text className="mb-1 text-[13px] text-[#A3A3A3]">End</Text>
|
||||
<View className="flex-row items-center">
|
||||
<Host matchContents>
|
||||
<DatePicker
|
||||
@@ -392,8 +418,12 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
onPress={handleAddSlot}
|
||||
className={`flex-row items-center justify-center rounded-xl py-3.5 ${
|
||||
transparentBackground
|
||||
? "border border-dashed border-gray-300/60 bg-white/40"
|
||||
: "border border-dashed border-[#C7C7CC] bg-white"
|
||||
? isDark
|
||||
? "border border-dashed border-[#4D4D4D]/60 bg-[#171717]/60"
|
||||
: "border border-dashed border-gray-300/60 bg-white/40"
|
||||
: isDark
|
||||
? "border border-dashed border-[#4D4D4D] bg-[#171717]"
|
||||
: "border border-dashed border-[#C7C7CC] bg-white"
|
||||
}`}
|
||||
>
|
||||
<Ionicons name="add-circle" size={20} color="#007AFF" />
|
||||
@@ -406,11 +436,17 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
{!isEnabled && (
|
||||
<View
|
||||
className={`items-center rounded-xl px-4 py-8 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
<Ionicons name="moon-outline" size={40} color="#8E8E93" />
|
||||
<Text className="mt-3 text-center text-[17px] text-[#8E8E93]">
|
||||
<Ionicons name="moon-outline" size={40} color="#A3A3A3" />
|
||||
<Text className="mt-3 text-center text-[17px] text-[#A3A3A3]">
|
||||
You are unavailable on {dayName}
|
||||
</Text>
|
||||
<Text className="mt-1 text-center text-[15px] text-[#C7C7CC]">
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { Alert, Platform, ScrollView, Switch, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
Platform,
|
||||
ScrollView,
|
||||
Switch,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
@@ -169,6 +178,18 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
EditAvailabilityDayScreenProps
|
||||
>(function EditAvailabilityDayScreen({ schedule, dayIndex, onSuccess, onSavingChange }, ref) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#FFFFFF",
|
||||
backgroundSecondary: isDark ? "#171717" : "#F9FAFB",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
iconColor: isDark ? "#FFFFFF" : "#A3A3A3",
|
||||
};
|
||||
|
||||
// Use mutation hook for cache-synchronized updates
|
||||
const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule();
|
||||
@@ -185,12 +206,12 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
// 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">
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-4 dark:border-[#4D4D4D]">
|
||||
<Text className="text-[17px] font-semibold text-black dark:text-white">
|
||||
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
|
||||
</Text>
|
||||
<AppPressable onPress={() => setShowTimePicker(null)}>
|
||||
<Ionicons name="close" size={24} color="#8E8E93" />
|
||||
<Ionicons name="close" size={24} color={colors.iconColor} />
|
||||
</AppPressable>
|
||||
</View>
|
||||
<ScrollView className="px-4 py-3">
|
||||
@@ -207,14 +228,16 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
<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"
|
||||
? "border-[#007AFF] bg-blue-50 shadow-md dark:bg-[#0A84FF]/20"
|
||||
: "border-gray-200 bg-gray-50 dark:border-[#4D4D4D] dark:bg-[#171717]"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-semibold text-[#007AFF]" : "font-medium text-gray-900"
|
||||
isSelected
|
||||
? "font-semibold text-[#007AFF]"
|
||||
: "font-medium text-gray-900 dark:text-white"
|
||||
}`}
|
||||
>
|
||||
{formatTime12Hour(time)}
|
||||
@@ -346,47 +369,54 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-white">
|
||||
<Text className="text-gray-500">No schedule data</Text>
|
||||
<View className="flex-1 items-center justify-center bg-white dark:bg-black">
|
||||
<Text className="text-gray-500 dark:text-[#A3A3A3]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-white"
|
||||
className="flex-1 bg-white dark:bg-black"
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: insets.bottom + 16,
|
||||
}}
|
||||
>
|
||||
{/* Enable/Disable Toggle */}
|
||||
<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>
|
||||
<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 dark:border-[#4D4D4D] dark:bg-[#171717]">
|
||||
<Text className="text-[17px] font-medium text-black dark:text-white">
|
||||
Available on {dayName}
|
||||
</Text>
|
||||
<Switch
|
||||
value={isEnabled}
|
||||
onValueChange={handleToggle}
|
||||
trackColor={{ false: "#E5E5EA", true: "#000000" }}
|
||||
thumbColor="#fff"
|
||||
trackColor={{
|
||||
false: isDark ? "#404040" : "#E5E5EA",
|
||||
true: isDark ? "#34C759" : "#000000",
|
||||
}}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Time Slots */}
|
||||
{isEnabled && (
|
||||
<>
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500">Time Slots</Text>
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500 dark:text-[#A3A3A3]">
|
||||
Time Slots
|
||||
</Text>
|
||||
|
||||
{slots.map((slot, index) => (
|
||||
<View
|
||||
key={`slot-${slot.startTime}-${slot.endTime}-${index}`}
|
||||
className="mb-3 rounded-xl border-2 border-gray-200 bg-white px-4 py-4"
|
||||
className="mb-3 rounded-xl border-2 border-gray-200 bg-white px-4 py-4 dark:border-[#4D4D4D] dark:bg-[#171717]"
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text className="text-[15px] font-medium text-gray-500">Slot {index + 1}</Text>
|
||||
<Text className="text-[15px] font-medium text-gray-500 dark:text-[#A3A3A3]">
|
||||
Slot {index + 1}
|
||||
</Text>
|
||||
{slots.length > 1 && (
|
||||
<AppPressable
|
||||
onPress={() => handleRemoveSlot(index)}
|
||||
className="rounded-full bg-red-50 p-2"
|
||||
className="rounded-full bg-red-50 p-2 dark:bg-[#FF453A]/20"
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
</AppPressable>
|
||||
@@ -395,8 +425,8 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
|
||||
<View className="mt-3 flex-row items-center">
|
||||
{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>
|
||||
<View className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100 dark:bg-[#2C2C2E] dark:border-[#4D4D4D]">
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">Start</Text>
|
||||
<input
|
||||
type="time"
|
||||
value={slot.startTime}
|
||||
@@ -408,7 +438,7 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: 500,
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
@@ -419,21 +449,21 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
</View>
|
||||
) : (
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100"
|
||||
className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100 dark:bg-[#2C2C2E] dark:border-[#4D4D4D]"
|
||||
onPress={() => setShowTimePicker({ slotIndex: index, type: "start" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">Start</Text>
|
||||
<Text className="text-[17px] font-medium text-black">
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">Start</Text>
|
||||
<Text className="text-[17px] font-medium text-black dark:text-white">
|
||||
{formatTime12Hour(slot.startTime)}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
)}
|
||||
|
||||
<Text className="mx-3 text-[17px] text-gray-400">–</Text>
|
||||
<Text className="mx-3 text-[17px] text-gray-400 dark:text-[#A3A3A3]">–</Text>
|
||||
|
||||
{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>
|
||||
<View className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100 dark:bg-[#2C2C2E] dark:border-[#4D4D4D]">
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">End</Text>
|
||||
<input
|
||||
type="time"
|
||||
value={slot.endTime}
|
||||
@@ -445,7 +475,7 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
style={{
|
||||
fontSize: 17,
|
||||
fontWeight: 500,
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
@@ -456,11 +486,11 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
</View>
|
||||
) : (
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100"
|
||||
className="flex-1 rounded-lg bg-gray-50 px-3 py-2.5 border border-gray-100 dark:bg-[#2C2C2E] dark:border-[#4D4D4D]"
|
||||
onPress={() => setShowTimePicker({ slotIndex: index, type: "end" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">End</Text>
|
||||
<Text className="text-[17px] font-medium text-black">
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">End</Text>
|
||||
<Text className="text-[17px] font-medium text-black dark:text-white">
|
||||
{formatTime12Hour(slot.endTime)}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
@@ -469,9 +499,8 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
</View>
|
||||
))}
|
||||
|
||||
{/* Add Slot Button */}
|
||||
<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">
|
||||
<View className="flex-row items-center justify-center rounded-xl border-2 border-dashed border-gray-300 bg-gray-50 py-4 dark:border-[#4D4D4D] dark:bg-[#171717]">
|
||||
<Ionicons name="add-circle" size={22} color="#007AFF" />
|
||||
<Text className="ml-2 text-[17px] font-medium text-[#007AFF]">Add Time Slot</Text>
|
||||
</View>
|
||||
@@ -479,22 +508,20 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Unavailable Message */}
|
||||
{!isEnabled && (
|
||||
<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 className="items-center rounded-xl border-2 border-dashed border-gray-200 px-4 py-12 dark:border-[#4D4D4D] dark:bg-[#171717]">
|
||||
<View className="items-center justify-center rounded-full bg-gray-50 p-4 dark:bg-[#2C2C2E]">
|
||||
<Ionicons name="moon-outline" size={32} color={colors.textSecondary} />
|
||||
</View>
|
||||
<Text className="mt-4 text-center text-[17px] font-medium text-gray-900">
|
||||
<Text className="mt-4 text-center text-[17px] font-medium text-gray-900 dark:text-white">
|
||||
Unavailable
|
||||
</Text>
|
||||
<Text className="mt-1 text-center text-[15px] text-gray-500">
|
||||
<Text className="mt-1 text-center text-[15px] text-gray-500 dark:text-[#A3A3A3]">
|
||||
You are currently unavailable on {dayName}.{"\n"}Toggle the switch above to add hours.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Time Picker Modal */}
|
||||
<FullScreenModal
|
||||
visible={!!showTimePicker}
|
||||
animationType={Platform.OS === "web" ? "fade" : "slide"}
|
||||
@@ -507,7 +534,7 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
onPress={() => setShowTimePicker(null)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2"
|
||||
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2 dark:bg-[#171717]"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={shadows.xl()}
|
||||
@@ -516,7 +543,7 @@ export const EditAvailabilityDayScreen = forwardRef<
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View className="flex-1 bg-white p-2">{renderTimePickerContent()}</View>
|
||||
<View className="flex-1 bg-white p-2 dark:bg-[#171717]">{renderTimePickerContent()}</View>
|
||||
)}
|
||||
</FullScreenModal>
|
||||
</ScrollView>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { forwardRef } from "react";
|
||||
import { ScrollView, Text, View } from "react-native";
|
||||
import { ScrollView, Text, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import type { Schedule } from "@/services/calcom";
|
||||
import type { ScheduleAvailability } from "@/services/types";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
@@ -90,34 +91,48 @@ export const EditAvailabilityHoursScreen = forwardRef<unknown, EditAvailabilityH
|
||||
_ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const backgroundColor = transparentBackground
|
||||
? "transparent"
|
||||
: isDark
|
||||
? theme.background
|
||||
: theme.backgroundMuted;
|
||||
|
||||
const availability = parseAvailability(schedule);
|
||||
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-[#8E8E93]">No schedule data</Text>
|
||||
<View className="flex-1 items-center justify-center" style={{ backgroundColor }}>
|
||||
<Text className="text-[#A3A3A3]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className={`flex-1 ${backgroundStyle}`}
|
||||
className="flex-1"
|
||||
style={{ backgroundColor }}
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: insets.bottom + 16,
|
||||
}}
|
||||
showsVerticalScrollIndicator={!transparentBackground}
|
||||
>
|
||||
<Text className="mb-3 px-1 text-[13px] font-medium text-[#8E8E93]">
|
||||
<Text className="mb-3 px-1 text-[13px] font-medium text-[#A3A3A3]">
|
||||
Tap a day to edit its hours
|
||||
</Text>
|
||||
|
||||
<View
|
||||
className={`overflow-hidden rounded-xl ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
{DAYS.map((day, dayIndex) => {
|
||||
@@ -128,44 +143,44 @@ export const EditAvailabilityHoursScreen = forwardRef<unknown, EditAvailabilityH
|
||||
<AppPressable
|
||||
key={day}
|
||||
className={`flex-row items-center px-4 py-3.5 ${
|
||||
dayIndex > 0 ? "border-t border-[#E5E5EA]" : ""
|
||||
dayIndex > 0
|
||||
? isDark
|
||||
? "border-t border-[#4D4D4D]"
|
||||
: "border-t border-[#E5E5EA]"
|
||||
: ""
|
||||
}`}
|
||||
onPress={() => onDayPress(dayIndex)}
|
||||
>
|
||||
{/* Day status indicator */}
|
||||
<View
|
||||
className={`mr-3 h-2.5 w-2.5 rounded-full ${
|
||||
isEnabled ? "bg-[#34C759]" : "bg-[#E5E5EA]"
|
||||
isEnabled ? "bg-[#34C759]" : isDark ? "bg-[#48484A]" : "bg-[#E5E5EA]"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Day name */}
|
||||
<Text
|
||||
className={`w-24 text-[17px] font-medium ${
|
||||
isEnabled ? "text-black" : "text-[#8E8E93]"
|
||||
isEnabled ? (isDark ? "text-white" : "text-black") : "text-[#A3A3A3]"
|
||||
}`}
|
||||
>
|
||||
{day}
|
||||
</Text>
|
||||
|
||||
{/* Time slots or unavailable */}
|
||||
<View className="flex-1">
|
||||
{isEnabled ? (
|
||||
daySlots.map((slot, slotIndex) => (
|
||||
<Text
|
||||
key={`${slotIndex}-${slot.startTime}`}
|
||||
className={`text-[15px] text-[#8E8E93] ${slotIndex > 0 ? "mt-0.5" : ""}`}
|
||||
className={`text-[15px] text-[#A3A3A3] ${slotIndex > 0 ? "mt-0.5" : ""}`}
|
||||
>
|
||||
{formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text className="text-[15px] text-[#8E8E93]">Unavailable</Text>
|
||||
<Text className="text-[15px] text-[#A3A3A3]">Unavailable</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Chevron */}
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={isDark ? "#A3A3A3" : "#C7C7CC"} />
|
||||
</AppPressable>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { forwardRef } from "react";
|
||||
import { ScrollView, Text, View } from "react-native";
|
||||
import { ScrollView, Text, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import type { Schedule } from "@/services/calcom";
|
||||
@@ -87,30 +87,32 @@ export interface EditAvailabilityHoursScreenProps {
|
||||
export const EditAvailabilityHoursScreen = forwardRef<unknown, EditAvailabilityHoursScreenProps>(
|
||||
function EditAvailabilityHoursScreen({ schedule, onDayPress }, _ref) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const availability = parseAvailability(schedule);
|
||||
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-white">
|
||||
<Text className="text-gray-500">No schedule data</Text>
|
||||
<View className="flex-1 items-center justify-center bg-white dark:bg-black">
|
||||
<Text className="text-gray-500 dark:text-[#A3A3A3]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-white"
|
||||
className="flex-1 bg-white dark:bg-black"
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: insets.bottom + 16,
|
||||
}}
|
||||
>
|
||||
<Text className="mb-3 text-[13px] font-medium text-gray-500">
|
||||
<Text className="mb-3 text-[13px] font-medium text-gray-500 dark:text-[#A3A3A3]">
|
||||
Tap a day to edit its hours
|
||||
</Text>
|
||||
|
||||
<View className="overflow-hidden rounded-xl bg-white border border-gray-200">
|
||||
<View className="overflow-hidden rounded-xl bg-white border border-gray-200 dark:bg-[#171717] dark:border-[#4D4D4D]">
|
||||
{DAYS.map((day, dayIndex) => {
|
||||
const daySlots = availability[dayIndex] || [];
|
||||
const isEnabled = daySlots.length > 0;
|
||||
@@ -119,43 +121,45 @@ export const EditAvailabilityHoursScreen = forwardRef<unknown, EditAvailabilityH
|
||||
<AppPressable key={day} onPress={() => onDayPress(dayIndex)}>
|
||||
<View
|
||||
className={`flex-row items-center px-4 py-3.5 ${
|
||||
dayIndex > 0 ? "border-t border-gray-100" : ""
|
||||
dayIndex > 0 ? "border-t border-gray-100 dark:border-[#4D4D4D]" : ""
|
||||
}`}
|
||||
>
|
||||
{/* Day status indicator */}
|
||||
<View
|
||||
className={`mr-3 h-2.5 w-2.5 rounded-full ${
|
||||
isEnabled ? "bg-green-500" : "bg-gray-300"
|
||||
isEnabled ? "bg-green-500" : "bg-gray-300 dark:bg-[#48484A]"
|
||||
}`}
|
||||
/>
|
||||
|
||||
{/* Day name */}
|
||||
<Text
|
||||
className={`w-28 text-[17px] font-medium ${
|
||||
isEnabled ? "text-black" : "text-gray-400"
|
||||
isEnabled ? "text-black dark:text-white" : "text-gray-400 dark:text-[#A3A3A3]"
|
||||
}`}
|
||||
>
|
||||
{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" : ""}`}
|
||||
className={`text-[15px] text-gray-600 dark:text-[#A3A3A3] ${slotIndex > 0 ? "mt-0.5" : ""}`}
|
||||
>
|
||||
{formatTime12Hour(slot.startTime)} – {formatTime12Hour(slot.endTime)}
|
||||
</Text>
|
||||
))
|
||||
) : (
|
||||
<Text className="text-[15px] text-gray-400">Unavailable</Text>
|
||||
<Text className="text-[15px] text-gray-400 dark:text-[#A3A3A3]">
|
||||
Unavailable
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Chevron */}
|
||||
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={20}
|
||||
color={isDark ? "#A3A3A3" : "#C7C7CC"}
|
||||
/>
|
||||
</View>
|
||||
</AppPressable>
|
||||
);
|
||||
|
||||
@@ -3,11 +3,20 @@ import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { Alert, KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native";
|
||||
import {
|
||||
Alert,
|
||||
KeyboardAvoidingView,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { TIMEZONES as ALL_TIMEZONES } from "@/constants/timezones";
|
||||
import { type Schedule, useUpdateSchedule } from "@/hooks/useSchedules";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
// Format timezones for display
|
||||
const TIMEZONES = ALL_TIMEZONES.map((tz) => ({
|
||||
@@ -34,7 +43,14 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const backgroundStyle = transparentBackground
|
||||
? "bg-transparent"
|
||||
: isDark
|
||||
? "bg-black"
|
||||
: `bg-[${theme.backgroundMuted}]`;
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [timezone, setTimezone] = useState("UTC");
|
||||
@@ -103,7 +119,7 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-[#8E8E93]">No schedule data</Text>
|
||||
<Text className="text-[#A3A3A3]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -122,12 +138,18 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
{transparentBackground ? (
|
||||
<>
|
||||
{/* Name Input - Glass UI */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#8E8E93]">Schedule Name</Text>
|
||||
<View className="mb-4 overflow-hidden rounded-xl border border-gray-300/40 bg-white/60">
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#A3A3A3]">Schedule Name</Text>
|
||||
<View
|
||||
className={`mb-4 overflow-hidden rounded-xl ${
|
||||
isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
}`}
|
||||
>
|
||||
<TextInput
|
||||
className="px-4 py-3.5 text-[17px] text-black"
|
||||
className={`px-4 py-3.5 text-[17px] ${isDark ? "text-white" : "text-black"}`}
|
||||
placeholder="Enter schedule name"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
autoCapitalize="words"
|
||||
@@ -136,14 +158,22 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
</View>
|
||||
|
||||
{/* Timezone Selector - Glass UI */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#8E8E93]">Timezone</Text>
|
||||
<View className="mb-4 flex-row items-center rounded-xl border border-gray-300/40 bg-white/60 px-4 py-3">
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#A3A3A3]">Timezone</Text>
|
||||
<View
|
||||
className={`mb-4 flex-row items-center rounded-xl px-4 py-3 ${
|
||||
isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
}`}
|
||||
>
|
||||
<View className="mr-3 h-9 w-9 items-center justify-center rounded-lg bg-[#007AFF]/20">
|
||||
<Ionicons name="globe-outline" size={20} color="#007AFF" />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[17px] font-medium text-black">{selectedTimezoneLabel}</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-[#8E8E93]">{timezone}</Text>
|
||||
<Text className={`text-[17px] font-medium ${isDark ? "text-white" : "text-black"}`}>
|
||||
{selectedTimezoneLabel}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-[#A3A3A3]">{timezone}</Text>
|
||||
</View>
|
||||
|
||||
{/* Native iOS Context Menu Button */}
|
||||
@@ -181,15 +211,16 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Name Input */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-[#8E8E93]">
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-[#A3A3A3]">
|
||||
Schedule Name
|
||||
</Text>
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className={`mb-4 overflow-hidden rounded-xl ${isDark ? "bg-[#171717]" : "bg-white"}`}
|
||||
>
|
||||
<TextInput
|
||||
className="px-4 py-3.5 text-[17px] text-black"
|
||||
className={`px-4 py-3.5 text-[17px] ${isDark ? "text-white" : "text-black"}`}
|
||||
placeholder="Enter schedule name"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
autoCapitalize="words"
|
||||
@@ -197,17 +228,20 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Timezone Selector */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-[#8E8E93]">
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-[#A3A3A3]">
|
||||
Timezone
|
||||
</Text>
|
||||
<View className="mb-4 flex-row items-center rounded-xl bg-white px-4 py-3">
|
||||
<View
|
||||
className={`mb-4 flex-row items-center rounded-xl px-4 py-3 ${isDark ? "bg-[#171717]" : "bg-white"}`}
|
||||
>
|
||||
<View className="mr-3 h-9 w-9 items-center justify-center rounded-lg bg-[#007AFF]/10">
|
||||
<Ionicons name="globe-outline" size={20} color="#007AFF" />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[17px] font-medium text-black">{selectedTimezoneLabel}</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-[#8E8E93]">{timezone}</Text>
|
||||
<Text className={`text-[17px] font-medium ${isDark ? "text-white" : "text-black"}`}>
|
||||
{selectedTimezoneLabel}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-[#A3A3A3]">{timezone}</Text>
|
||||
</View>
|
||||
|
||||
{/* Native iOS Context Menu Button */}
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@@ -40,6 +41,8 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
EditAvailabilityNameScreenProps
|
||||
>(function EditAvailabilityNameScreen({ schedule, onSuccess, onSavingChange }, ref) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const [name, setName] = useState("");
|
||||
const [timezone, setTimezone] = useState("UTC");
|
||||
@@ -104,10 +107,12 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
// 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>
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-4 dark:border-[#4D4D4D]">
|
||||
<Text className="text-[17px] font-semibold text-black dark:text-white">
|
||||
Select Timezone
|
||||
</Text>
|
||||
<AppPressable onPress={() => setShowTimezoneModal(false)}>
|
||||
<Ionicons name="close" size={24} color="#8E8E93" />
|
||||
<Ionicons name="close" size={24} color={isDark ? "#FFFFFF" : "#A3A3A3"} />
|
||||
</AppPressable>
|
||||
</View>
|
||||
<ScrollView className="px-4 py-3">
|
||||
@@ -122,8 +127,8 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
<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"
|
||||
? "border-[#007AFF] bg-blue-50 shadow-md dark:bg-[#0A84FF]/20"
|
||||
: "border-gray-200 bg-gray-50 dark:border-[#4D4D4D] dark:bg-[#171717]"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
@@ -132,12 +137,14 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
className={`text-[17px] ${
|
||||
tz.id === timezone
|
||||
? "font-semibold text-[#007AFF]"
|
||||
: "font-medium text-gray-900"
|
||||
: "font-medium text-gray-900 dark:text-white"
|
||||
}`}
|
||||
>
|
||||
{tz.label}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">{tz.id}</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500 dark:text-[#A3A3A3]">
|
||||
{tz.id}
|
||||
</Text>
|
||||
</View>
|
||||
{tz.id === timezone && (
|
||||
<View className="rounded-full bg-[#007AFF] p-1.5">
|
||||
@@ -154,14 +161,14 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-white">
|
||||
<Text className="text-gray-500">No schedule data</Text>
|
||||
<View className="flex-1 items-center justify-center bg-white dark:bg-black">
|
||||
<Text className="text-gray-500 dark:text-[#A3A3A3]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView behavior="padding" className="flex-1 bg-white">
|
||||
<KeyboardAvoidingView behavior="padding" className="flex-1 bg-white dark:bg-black">
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{
|
||||
@@ -170,26 +177,24 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* Name Input */}
|
||||
<Text className="mb-2 text-[13px] font-medium uppercase tracking-wide text-gray-500">
|
||||
<Text className="mb-2 text-[13px] font-medium uppercase tracking-wide text-gray-500 dark:text-[#A3A3A3]">
|
||||
Schedule Name
|
||||
</Text>
|
||||
<TextInput
|
||||
className="mb-4 rounded-lg border border-gray-300 bg-gray-50 px-4 py-3 text-[17px] text-black"
|
||||
className="mb-4 rounded-lg border border-gray-300 bg-gray-50 px-4 py-3 text-[17px] text-black dark:border-[#4D4D4D] dark:bg-[#171717] dark:text-white"
|
||||
placeholder="Enter schedule name"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
autoCapitalize="words"
|
||||
editable={!isSaving}
|
||||
/>
|
||||
|
||||
{/* Timezone Selector */}
|
||||
<Text className="mb-2 text-[13px] font-medium uppercase tracking-wide text-gray-500">
|
||||
<Text className="mb-2 text-[13px] font-medium uppercase tracking-wide text-gray-500 dark:text-[#A3A3A3]">
|
||||
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 justify-between rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-4 dark:border-[#4D4D4D] dark:bg-[#171717]">
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons
|
||||
name="globe-outline"
|
||||
@@ -198,16 +203,19 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
style={{ marginRight: 12 }}
|
||||
/>
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">{selectedTimezoneLabel}</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">{timezone}</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">
|
||||
{selectedTimezoneLabel}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500 dark:text-[#A3A3A3]">
|
||||
{timezone}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Ionicons name="chevron-down" size={20} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-down" size={20} color={isDark ? "#A3A3A3" : "#C7C7CC"} />
|
||||
</View>
|
||||
</AppPressable>
|
||||
</ScrollView>
|
||||
|
||||
{/* Timezone Modal */}
|
||||
<FullScreenModal
|
||||
visible={showTimezoneModal}
|
||||
animationType={Platform.OS === "web" ? "fade" : "slide"}
|
||||
@@ -220,7 +228,7 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
onPress={() => setShowTimezoneModal(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2"
|
||||
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2 dark:bg-[#171717]"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={shadows.xl()}
|
||||
@@ -229,7 +237,7 @@ export const EditAvailabilityNameScreen = forwardRef<
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View className="flex-1 bg-white p-2">{renderTimezoneContent()}</View>
|
||||
<View className="flex-1 bg-white p-2 dark:bg-[#171717]">{renderTimezoneContent()}</View>
|
||||
)}
|
||||
</FullScreenModal>
|
||||
</KeyboardAvoidingView>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { Alert, Pressable, ScrollView, Switch, Text, View } from "react-native";
|
||||
import { Alert, Pressable, ScrollView, Switch, Text, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { DateTimePickerAndroid } from "@react-native-community/datetimepicker";
|
||||
import { useUpdateSchedule } from "@/hooks/useSchedules";
|
||||
import type { Schedule } from "@/services/calcom";
|
||||
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
// Convert 24-hour time to 12-hour format with AM/PM
|
||||
const formatTime12Hour = (time24: string): string => {
|
||||
@@ -92,7 +93,14 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const backgroundStyle = transparentBackground
|
||||
? "bg-transparent"
|
||||
: isDark
|
||||
? "bg-black"
|
||||
: `bg-[${theme.backgroundMuted}]`;
|
||||
|
||||
// Use mutation hook for cache-synchronized updates
|
||||
const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule();
|
||||
@@ -337,7 +345,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-[#8E8E93]">No schedule data</Text>
|
||||
<Text className="text-[#A3A3A3]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -351,12 +359,17 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
}}
|
||||
showsVerticalScrollIndicator={!transparentBackground}
|
||||
>
|
||||
{/* Date Picker */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#8E8E93]">Date</Text>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#A3A3A3]">Date</Text>
|
||||
<Pressable
|
||||
onPress={openDatePicker}
|
||||
className={`mb-4 rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
@@ -364,16 +377,23 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
<View className="mr-3 h-9 w-9 items-center justify-center rounded-lg bg-[#007AFF]/10">
|
||||
<Ionicons name="calendar" size={20} color="#007AFF" />
|
||||
</View>
|
||||
<Text className="text-[17px] text-black">{formatDateForDisplay(selectedDate)}</Text>
|
||||
<Text className={`text-[17px] ${isDark ? "text-white" : "text-black"}`}>
|
||||
{formatDateForDisplay(selectedDate)}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={isDark ? "#A3A3A3" : "#C7C7CC"} />
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
{/* Unavailable Toggle */}
|
||||
<View
|
||||
className={`mb-4 flex-row items-center justify-between rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
@@ -381,40 +401,50 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
<Ionicons name="moon-outline" size={20} color="#FF3B30" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">Mark as unavailable</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Block this entire day</Text>
|
||||
<Text className={`text-[17px] ${isDark ? "text-white" : "text-black"}`}>
|
||||
Mark as unavailable
|
||||
</Text>
|
||||
<Text className="text-[13px] text-[#A3A3A3]">Block this entire day</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={isUnavailable}
|
||||
onValueChange={setIsUnavailable}
|
||||
trackColor={{ false: "#E5E5EA", true: "#000000" }}
|
||||
thumbColor="#fff"
|
||||
trackColor={{
|
||||
false: isDark ? theme.backgroundEmphasis : "#E5E5EA",
|
||||
true: isDark ? "#34C759" : "#000000",
|
||||
}}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Time Range (only when available) */}
|
||||
{!isUnavailable && (
|
||||
<>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#8E8E93]">Available Hours</Text>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#A3A3A3]">Available Hours</Text>
|
||||
<View
|
||||
className={`mb-4 rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<Pressable onPress={openStartTimePicker} className="flex-1">
|
||||
<Text className="mb-1 text-[13px] text-[#8E8E93]">Start Time</Text>
|
||||
<Text className="text-[17px] text-black">
|
||||
<Text className="mb-1 text-[13px] text-[#A3A3A3]">Start Time</Text>
|
||||
<Text className={`text-[17px] ${isDark ? "text-white" : "text-black"}`}>
|
||||
{formatTime12Hour(dateToTimeString(startTime))}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
<Text className="mx-4 text-[17px] text-[#8E8E93]">–</Text>
|
||||
<Text className="mx-4 text-[17px] text-[#A3A3A3]">–</Text>
|
||||
|
||||
<Pressable onPress={openEndTimePicker} className="flex-1">
|
||||
<Text className="mb-1 text-[13px] text-[#8E8E93]">End Time</Text>
|
||||
<Text className="text-[17px] text-black">
|
||||
<Text className="mb-1 text-[13px] text-[#A3A3A3]">End Time</Text>
|
||||
<Text className={`text-[17px] ${isDark ? "text-white" : "text-black"}`}>
|
||||
{formatTime12Hour(dateToTimeString(endTime))}
|
||||
</Text>
|
||||
</Pressable>
|
||||
@@ -423,34 +453,43 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Existing Overrides (if any) */}
|
||||
{!isEditing && schedule.overrides && schedule.overrides.length > 0 && (
|
||||
<>
|
||||
<Text className="mb-2 mt-4 px-1 text-[13px] font-medium text-[#8E8E93]">
|
||||
<Text className="mb-2 mt-4 px-1 text-[13px] font-medium text-[#A3A3A3]">
|
||||
Existing Overrides
|
||||
</Text>
|
||||
<View
|
||||
className={`overflow-hidden rounded-xl ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
{schedule.overrides.map((override, index) => (
|
||||
<Pressable
|
||||
key={override.date}
|
||||
className={`flex-row items-center justify-between px-4 py-3 ${
|
||||
index > 0 ? "border-t border-[#E5E5EA]" : ""
|
||||
index > 0
|
||||
? isDark
|
||||
? "border-t border-[#4D4D4D]"
|
||||
: "border-t border-[#E5E5EA]"
|
||||
: ""
|
||||
}`}
|
||||
onPress={() => onEditOverride?.(index)}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[15px] text-black">
|
||||
<Text className={`text-[15px] ${isDark ? "text-white" : "text-black"}`}>
|
||||
{formatDateForDisplay(dateStringToDate(override.date ?? ""))}
|
||||
</Text>
|
||||
{normalizeTimeString(override.startTime ?? "00:00") === "00:00" &&
|
||||
normalizeTimeString(override.endTime ?? "00:00") === "00:00" ? (
|
||||
<Text className="text-[13px] text-[#FF3B30]">Unavailable</Text>
|
||||
) : (
|
||||
<Text className="text-[13px] text-[#8E8E93]">
|
||||
<Text className="text-[13px] text-[#A3A3A3]">
|
||||
{formatTime12Hour(normalizeTimeString(override.startTime ?? "00:00"))} –{" "}
|
||||
{formatTime12Hour(normalizeTimeString(override.endTime ?? "00:00"))}
|
||||
</Text>
|
||||
@@ -464,10 +503,10 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
className="ml-3 rounded-full bg-[#8E8E93]/10 p-2"
|
||||
className="ml-3 rounded-full bg-[#A3A3A3]/10 p-2"
|
||||
onPress={() => onEditOverride?.(index)}
|
||||
>
|
||||
<Ionicons name="chevron-forward" size={18} color="#8E8E93" />
|
||||
<Ionicons name="chevron-forward" size={18} color="#A3A3A3" />
|
||||
</Pressable>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { DatePicker, Host } from "@expo/ui/swift-ui";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { Alert, Pressable, ScrollView, Switch, Text, View } from "react-native";
|
||||
import { Alert, Pressable, ScrollView, Switch, Text, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useUpdateSchedule } from "@/hooks/useSchedules";
|
||||
import type { Schedule } from "@/services/calcom";
|
||||
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
// Convert 24-hour time to 12-hour format with AM/PM
|
||||
const formatTime12Hour = (time24: string): string => {
|
||||
@@ -87,7 +88,14 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const backgroundColor = transparentBackground
|
||||
? "transparent"
|
||||
: isDark
|
||||
? theme.background
|
||||
: theme.backgroundMuted;
|
||||
|
||||
// Use mutation hook for cache-synchronized updates
|
||||
const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule();
|
||||
@@ -275,26 +283,32 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-[#8E8E93]">No schedule data</Text>
|
||||
<View className="flex-1 items-center justify-center" style={{ backgroundColor }}>
|
||||
<Text className="text-[#A3A3A3]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className={`flex-1 ${backgroundStyle}`}
|
||||
className="flex-1"
|
||||
style={{ backgroundColor }}
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: insets.bottom + 16,
|
||||
}}
|
||||
showsVerticalScrollIndicator={!transparentBackground}
|
||||
>
|
||||
{/* Date Picker */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#8E8E93]">Date</Text>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#A3A3A3]">Date</Text>
|
||||
<View
|
||||
className={`mb-4 rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
@@ -302,7 +316,9 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
<View className="mr-3 h-9 w-9 items-center justify-center rounded-lg bg-[#007AFF]/10">
|
||||
<Ionicons name="calendar" size={20} color="#007AFF" />
|
||||
</View>
|
||||
<Text className="text-[17px] text-black">{formatDateForDisplay(selectedDate)}</Text>
|
||||
<Text className={`text-[17px] ${isDark ? "text-white" : "text-black"}`}>
|
||||
{formatDateForDisplay(selectedDate)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View className="mt-3">
|
||||
@@ -316,10 +332,15 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Unavailable Toggle */}
|
||||
<View
|
||||
className={`mb-4 flex-row items-center justify-between rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
@@ -327,30 +348,40 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
<Ionicons name="moon-outline" size={20} color="#FF3B30" />
|
||||
</View>
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">Mark as unavailable</Text>
|
||||
<Text className="text-[13px] text-[#8E8E93]">Block this entire day</Text>
|
||||
<Text className={`text-[17px] ${isDark ? "text-white" : "text-black"}`}>
|
||||
Mark as unavailable
|
||||
</Text>
|
||||
<Text className="text-[13px] text-[#A3A3A3]">Block this entire day</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={isUnavailable}
|
||||
onValueChange={setIsUnavailable}
|
||||
trackColor={{ false: "#E5E5EA", true: "#000000" }}
|
||||
thumbColor="#fff"
|
||||
trackColor={{
|
||||
false: isDark ? theme.backgroundEmphasis : "#E5E5EA",
|
||||
true: isDark ? "#34C759" : "#000000",
|
||||
}}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Time Range (only when available) */}
|
||||
{!isUnavailable && (
|
||||
<>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#8E8E93]">Available Hours</Text>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-[#A3A3A3]">Available Hours</Text>
|
||||
<View
|
||||
className={`mb-4 rounded-xl px-4 py-3.5 ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<View className="flex-1">
|
||||
<Text className="mb-2 text-[13px] text-[#8E8E93]">Start Time</Text>
|
||||
<Text className="mb-2 text-[13px] text-[#A3A3A3]">Start Time</Text>
|
||||
<Host matchContents>
|
||||
<DatePicker
|
||||
onDateChange={handleStartTimeChange}
|
||||
@@ -360,10 +391,10 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</Host>
|
||||
</View>
|
||||
|
||||
<Text className="mx-4 text-[17px] text-[#8E8E93]">–</Text>
|
||||
<Text className="mx-4 text-[17px] text-[#A3A3A3]">–</Text>
|
||||
|
||||
<View className="flex-1">
|
||||
<Text className="mb-2 text-[13px] text-[#8E8E93]">End Time</Text>
|
||||
<Text className="mb-2 text-[13px] text-[#A3A3A3]">End Time</Text>
|
||||
<Host matchContents>
|
||||
<DatePicker
|
||||
onDateChange={handleEndTimeChange}
|
||||
@@ -377,33 +408,42 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Existing Overrides (if any) */}
|
||||
{!isEditing && schedule.overrides && schedule.overrides.length > 0 && (
|
||||
<>
|
||||
<Text className="mb-2 mt-4 px-1 text-[13px] font-medium text-[#8E8E93]">
|
||||
<Text className="mb-2 mt-4 px-1 text-[13px] font-medium text-[#A3A3A3]">
|
||||
Existing Overrides
|
||||
</Text>
|
||||
<View
|
||||
className={`overflow-hidden rounded-xl ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
{schedule.overrides.map((override, index) => (
|
||||
<Pressable
|
||||
key={override.date}
|
||||
className={`flex-row items-center justify-between px-4 py-3 ${
|
||||
index > 0 ? "border-t border-[#E5E5EA]" : ""
|
||||
index > 0
|
||||
? isDark
|
||||
? "border-t border-[#4D4D4D]"
|
||||
: "border-t border-[#E5E5EA]"
|
||||
: ""
|
||||
}`}
|
||||
onPress={() => onEditOverride?.(index)}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[15px] text-black">
|
||||
<Text className={`text-[15px] ${isDark ? "text-white" : "text-black"}`}>
|
||||
{formatDateForDisplay(dateStringToDate(override.date ?? ""))}
|
||||
</Text>
|
||||
{override.startTime === "00:00" && override.endTime === "00:00" ? (
|
||||
<Text className="text-[13px] text-[#FF3B30]">Unavailable</Text>
|
||||
) : (
|
||||
<Text className="text-[13px] text-[#8E8E93]">
|
||||
<Text className="text-[13px] text-[#A3A3A3]">
|
||||
{formatTime12Hour(override.startTime ?? "00:00")} –{" "}
|
||||
{formatTime12Hour(override.endTime ?? "00:00")}
|
||||
</Text>
|
||||
@@ -417,10 +457,10 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
className="ml-3 rounded-full bg-[#8E8E93]/10 p-2"
|
||||
className="ml-3 rounded-full bg-[#A3A3A3]/10 p-2"
|
||||
onPress={() => onEditOverride?.(index)}
|
||||
>
|
||||
<Ionicons name="chevron-forward" size={18} color="#8E8E93" />
|
||||
<Ionicons name="chevron-forward" size={18} color="#A3A3A3" />
|
||||
</Pressable>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@@ -76,6 +77,8 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
// Use mutation hook for cache-synchronized updates
|
||||
const { mutate: updateSchedule, isPending: isMutating } = useUpdateSchedule();
|
||||
@@ -93,12 +96,12 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
// 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">
|
||||
<View className="flex-row items-center justify-between border-b border-gray-200 px-4 py-3 dark:border-[#4D4D4D]">
|
||||
<Text className="text-[17px] font-semibold text-black dark:text-white">
|
||||
Select {showTimePicker?.type === "start" ? "Start" : "End"} Time
|
||||
</Text>
|
||||
<AppPressable onPress={() => setShowTimePicker(null)}>
|
||||
<Ionicons name="close" size={24} color="#8E8E93" />
|
||||
<Ionicons name="close" size={24} color={isDark ? "#FFFFFF" : "#A3A3A3"} />
|
||||
</AppPressable>
|
||||
</View>
|
||||
<ScrollView className="px-4 py-3">
|
||||
@@ -111,14 +114,16 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
<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"
|
||||
? "border-[#007AFF] bg-blue-50 shadow-md dark:bg-[#0A84FF]/20"
|
||||
: "border-gray-200 bg-gray-50 dark:border-[#4D4D4D] dark:bg-[#171717]"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-semibold text-[#007AFF]" : "font-medium text-gray-900"
|
||||
isSelected
|
||||
? "font-semibold text-[#007AFF]"
|
||||
: "font-medium text-gray-900 dark:text-white"
|
||||
}`}
|
||||
>
|
||||
{formatTime12Hour(time)}
|
||||
@@ -320,23 +325,24 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
|
||||
if (!schedule) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-white">
|
||||
<Text className="text-gray-500">No schedule data</Text>
|
||||
<View className="flex-1 items-center justify-center bg-white dark:bg-black">
|
||||
<Text className="text-gray-500 dark:text-[#A3A3A3]">No schedule data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
className="flex-1 bg-white"
|
||||
className="flex-1 bg-white dark:bg-black"
|
||||
contentContainerStyle={{
|
||||
padding: 16,
|
||||
paddingBottom: insets.bottom + 16,
|
||||
}}
|
||||
>
|
||||
{/* Date Input */}
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500">Date (YYYY-MM-DD)</Text>
|
||||
<View className="mb-4 rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3">
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500 dark:text-[#A3A3A3]">
|
||||
Date (YYYY-MM-DD)
|
||||
</Text>
|
||||
<View className="mb-4 rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3 dark:border-[#4D4D4D] dark:bg-[#171717]">
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons name="calendar-outline" size={20} color="#007AFF" style={{ marginRight: 12 }} />
|
||||
{Platform.OS === "web" ? (
|
||||
@@ -347,7 +353,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 17,
|
||||
color: "#000",
|
||||
color: isDark ? "#FFFFFF" : "#000000",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
@@ -356,54 +362,59 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
className="flex-1 text-[17px] text-black"
|
||||
className="flex-1 text-[17px] text-black dark:text-white"
|
||||
placeholder="2024-12-25"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={isDark ? "#A3A3A3" : "#9CA3AF"}
|
||||
value={selectedDate}
|
||||
onChangeText={setSelectedDate}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
{selectedDate ? (
|
||||
<Text className="mt-2 text-[15px] text-gray-500">
|
||||
<Text className="mt-2 text-[15px] text-gray-500 dark:text-[#A3A3A3]">
|
||||
{formatDateForDisplay(selectedDate)}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
|
||||
{/* Unavailable Toggle */}
|
||||
<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="mb-4 flex-row items-center justify-between rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3.5 dark:border-[#4D4D4D] dark:bg-[#171717]">
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons name="moon-outline" size={20} color="#FF3B30" style={{ marginRight: 12 }} />
|
||||
<View>
|
||||
<Text className="text-[17px] text-black">Mark as unavailable</Text>
|
||||
<Text className="text-[13px] text-gray-500">Block this entire day</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">Mark as unavailable</Text>
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">
|
||||
Block this entire day
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Switch
|
||||
value={isUnavailable}
|
||||
onValueChange={setIsUnavailable}
|
||||
trackColor={{ false: "#E5E5EA", true: "#000000" }}
|
||||
thumbColor="#fff"
|
||||
trackColor={{
|
||||
false: isDark ? "#404040" : "#E5E5EA",
|
||||
true: isDark ? "#34C759" : "#000000",
|
||||
}}
|
||||
thumbColor="#FFFFFF"
|
||||
/>
|
||||
</View>
|
||||
|
||||
{/* Time Range (only when available) */}
|
||||
{!isUnavailable && (
|
||||
<>
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500">Available Hours</Text>
|
||||
<View className="mb-4 rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3.5">
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500 dark:text-[#A3A3A3]">
|
||||
Available Hours
|
||||
</Text>
|
||||
<View className="mb-4 rounded-xl border-2 border-gray-200 bg-gray-50 px-4 py-3.5 dark:border-[#4D4D4D] dark:bg-[#171717]">
|
||||
<View className="flex-row items-center">
|
||||
{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>
|
||||
<View className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2 dark:bg-[#4D4D4D]/50">
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">Start Time</Text>
|
||||
<input
|
||||
type="time"
|
||||
value={startTime}
|
||||
onChange={(e) => setStartTime(e.target.value)}
|
||||
style={{
|
||||
fontSize: 17,
|
||||
color: "#000",
|
||||
color: isDark ? "#FFFFFF" : "#000000",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
@@ -414,26 +425,28 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</View>
|
||||
) : (
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2"
|
||||
className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2 dark:bg-[#4D4D4D]/50"
|
||||
onPress={() => setShowTimePicker({ type: "start" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">Start Time</Text>
|
||||
<Text className="text-[17px] text-black">{formatTime12Hour(startTime)}</Text>
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">Start Time</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">
|
||||
{formatTime12Hour(startTime)}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
)}
|
||||
|
||||
<Text className="mx-4 text-[17px] text-gray-400">–</Text>
|
||||
<Text className="mx-4 text-[17px] text-gray-400 dark:text-[#A3A3A3]">–</Text>
|
||||
|
||||
{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>
|
||||
<View className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2 dark:bg-[#4D4D4D]/50">
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">End Time</Text>
|
||||
<input
|
||||
type="time"
|
||||
value={endTime}
|
||||
onChange={(e) => setEndTime(e.target.value)}
|
||||
style={{
|
||||
fontSize: 17,
|
||||
color: "#000",
|
||||
color: isDark ? "#FFFFFF" : "#000000",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
outline: "none",
|
||||
@@ -444,11 +457,13 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</View>
|
||||
) : (
|
||||
<AppPressable
|
||||
className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2"
|
||||
className="flex-1 rounded-lg bg-gray-200/50 px-3 py-2 dark:bg-[#4D4D4D]/50"
|
||||
onPress={() => setShowTimePicker({ type: "end" })}
|
||||
>
|
||||
<Text className="text-[13px] text-gray-500">End Time</Text>
|
||||
<Text className="text-[17px] text-black">{formatTime12Hour(endTime)}</Text>
|
||||
<Text className="text-[13px] text-gray-500 dark:text-[#A3A3A3]">End Time</Text>
|
||||
<Text className="text-[17px] text-black dark:text-white">
|
||||
{formatTime12Hour(endTime)}
|
||||
</Text>
|
||||
</AppPressable>
|
||||
)}
|
||||
</View>
|
||||
@@ -456,10 +471,9 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Existing Overrides */}
|
||||
{!isEditing && schedule.overrides && schedule.overrides.length > 0 && (
|
||||
<>
|
||||
<Text className="mb-2 mt-4 text-[13px] font-medium text-gray-500">
|
||||
<Text className="mb-2 mt-4 text-[13px] font-medium text-gray-500 dark:text-[#A3A3A3]">
|
||||
Existing Overrides
|
||||
</Text>
|
||||
<View>
|
||||
@@ -469,15 +483,17 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
className="mb-2.5"
|
||||
onPress={() => onEditOverride?.(index)}
|
||||
>
|
||||
<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-row items-center justify-between rounded-xl border-2 border-gray-200 bg-white px-4 py-3.5 dark:border-[#4D4D4D] dark:bg-[#171717]">
|
||||
<View className="flex-1">
|
||||
<Text className="text-[15px] font-medium text-black">
|
||||
<Text className="text-[15px] font-medium text-black dark:text-white">
|
||||
{formatDateForDisplay(override.date ?? "")}
|
||||
</Text>
|
||||
{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-red-500 dark:text-[#FF453A]">
|
||||
Unavailable
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500 dark:text-[#A3A3A3]">
|
||||
{formatTime12Hour(override.startTime ?? "00:00")} –{" "}
|
||||
{formatTime12Hour(override.endTime ?? "00:00")}
|
||||
</Text>
|
||||
@@ -485,7 +501,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</View>
|
||||
<View className="flex-row items-center">
|
||||
<AppPressable
|
||||
className="rounded-full bg-red-50 p-2"
|
||||
className="rounded-full bg-red-50 p-2 dark:bg-[#FF453A]/10"
|
||||
onPress={() => handleDeleteOverride(index)}
|
||||
>
|
||||
<Ionicons name="trash-outline" size={18} color="#FF3B30" />
|
||||
@@ -494,7 +510,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
style={{ marginLeft: 12 }}
|
||||
name="chevron-forward"
|
||||
size={18}
|
||||
color="#C7C7CC"
|
||||
color={isDark ? "#A3A3A3" : "#C7C7CC"}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
@@ -504,7 +520,6 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Time Picker Modal */}
|
||||
<FullScreenModal
|
||||
visible={!!showTimePicker}
|
||||
animationType={Platform.OS === "web" ? "fade" : "slide"}
|
||||
@@ -517,7 +532,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
onPress={() => setShowTimePicker(null)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2"
|
||||
className="max-h-[80%] w-full max-w-[500px] overflow-hidden rounded-2xl bg-white p-2 dark:bg-[#171717]"
|
||||
activeOpacity={1}
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
style={shadows.xl()}
|
||||
@@ -526,7 +541,7 @@ export const EditAvailabilityOverrideScreen = forwardRef<
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<View className="flex-1 bg-white p-2">{renderTimePickerContent()}</View>
|
||||
<View className="flex-1 bg-white p-2 dark:bg-[#171717]">{renderTimePickerContent()}</View>
|
||||
)}
|
||||
</FullScreenModal>
|
||||
</ScrollView>
|
||||
|
||||
@@ -10,7 +10,14 @@ import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native";
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useUpdateLocation } from "@/hooks/useBookings";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
@@ -94,7 +101,21 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#F2F2F7",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
pill: isDark ? "#4D4D4D" : "#E8E8ED",
|
||||
inputBorder: isDark ? "rgba(56, 56, 58, 0.4)" : "rgba(209, 213, 219, 0.4)",
|
||||
inputBackground: isDark ? "rgba(28, 28, 30, 0.6)" : "rgba(255, 255, 255, 0.6)",
|
||||
accent: "#007AFF",
|
||||
};
|
||||
|
||||
const [selectedType, setSelectedType] = useState<LocationTypeId>("link");
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
|
||||
@@ -191,14 +212,21 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
|
||||
if (!booking) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-gray-500">No booking data</Text>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: transparentBackground ? "transparent" : colors.background }}
|
||||
>
|
||||
<Text style={{ color: colors.textSecondary }}>No booking data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView behavior="padding" className={`flex-1 ${backgroundStyle}`}>
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
className="flex-1"
|
||||
style={{ backgroundColor: transparentBackground ? "transparent" : colors.background }}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{
|
||||
@@ -211,15 +239,27 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
{transparentBackground ? (
|
||||
<>
|
||||
{/* Location Type Selector with Native Context Menu - Glass UI */}
|
||||
<View className="mb-4 flex-row items-center rounded-xl border border-gray-300/40 bg-white/60 px-4 py-3">
|
||||
<View className="mr-3 h-9 w-9 items-center justify-center rounded-lg bg-[#007AFF]/20">
|
||||
<Ionicons name={selectedTypeConfig.iconName} size={20} color="#007AFF" />
|
||||
<View
|
||||
className="mb-4 flex-row items-center rounded-xl px-4 py-3"
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.inputBorder,
|
||||
backgroundColor: colors.inputBackground,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-9 w-9 items-center justify-center rounded-lg"
|
||||
style={{
|
||||
backgroundColor: isDark ? "rgba(0, 122, 255, 0.3)" : "rgba(0, 122, 255, 0.2)",
|
||||
}}
|
||||
>
|
||||
<Ionicons name={selectedTypeConfig.iconName} size={20} color={colors.accent} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[17px] font-medium text-[#000]">
|
||||
<Text className="text-[17px] font-medium" style={{ color: colors.text }}>
|
||||
{selectedTypeConfig.label}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">
|
||||
<Text className="mt-0.5 text-[13px]" style={{ color: colors.textSecondary }}>
|
||||
{selectedTypeConfig.description}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -265,13 +305,21 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
</View>
|
||||
|
||||
{/* Location Input - Glass UI */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl border border-gray-300/40 bg-white/60">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.inputBorder,
|
||||
backgroundColor: colors.inputBackground,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
className={`px-4 py-3 text-[17px] text-[#000] ${
|
||||
className={`px-4 py-3 text-[17px] ${
|
||||
selectedTypeConfig.inputType === "multiline" ? "min-h-[100px]" : "h-[50px]"
|
||||
}`}
|
||||
style={{ color: colors.text }}
|
||||
placeholder={selectedTypeConfig.placeholder}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={inputValue}
|
||||
onChangeText={setInputValue}
|
||||
keyboardType={selectedTypeConfig.keyboardType}
|
||||
@@ -289,13 +337,28 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
<>
|
||||
{/* Current Location Info */}
|
||||
{booking.location && (
|
||||
<View className="mb-4 flex-row items-start rounded-xl bg-white p-4">
|
||||
<View className="mr-3 h-10 w-10 items-center justify-center rounded-full bg-[#E8E8ED]">
|
||||
<Ionicons name="location" size={20} color="#6B7280" />
|
||||
<View
|
||||
className="mb-4 flex-row items-start rounded-xl p-4"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-10 w-10 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: colors.pill }}
|
||||
>
|
||||
<Ionicons name="location" size={20} color={colors.textSecondary} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[13px] font-medium text-gray-500">Current Location</Text>
|
||||
<Text className="mt-0.5 text-[15px] text-[#000]" numberOfLines={2}>
|
||||
<Text
|
||||
className="text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Current Location
|
||||
</Text>
|
||||
<Text
|
||||
className="mt-0.5 text-[15px]"
|
||||
style={{ color: colors.text }}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.location}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -303,18 +366,29 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
)}
|
||||
|
||||
{/* Location Type Selector with Native Context Menu */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-gray-500">
|
||||
<Text
|
||||
className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Location Type
|
||||
</Text>
|
||||
<View className="mb-4 flex-row items-center rounded-xl bg-white px-4">
|
||||
<View className="mr-3 h-9 w-9 items-center justify-center rounded-lg bg-[#007AFF]/10">
|
||||
<Ionicons name={selectedTypeConfig.iconName} size={20} color="#007AFF" />
|
||||
<View
|
||||
className="mb-4 flex-row items-center rounded-xl px-4"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-9 w-9 items-center justify-center rounded-lg"
|
||||
style={{
|
||||
backgroundColor: isDark ? "rgba(0, 122, 255, 0.2)" : "rgba(0, 122, 255, 0.1)",
|
||||
}}
|
||||
>
|
||||
<Ionicons name={selectedTypeConfig.iconName} size={20} color={colors.accent} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[17px] font-medium text-[#000]">
|
||||
<Text className="text-[17px] font-medium" style={{ color: colors.text }}>
|
||||
{selectedTypeConfig.label}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">
|
||||
<Text className="mt-0.5 text-[13px]" style={{ color: colors.textSecondary }}>
|
||||
{selectedTypeConfig.description}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -360,16 +434,23 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
</View>
|
||||
|
||||
{/* Location Input */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-gray-500">
|
||||
<Text
|
||||
className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
{selectedTypeConfig.label}
|
||||
</Text>
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<TextInput
|
||||
className={`px-4 py-3 text-[17px] text-[#000] ${
|
||||
className={`px-4 py-3 text-[17px] ${
|
||||
selectedTypeConfig.inputType === "multiline" ? "min-h-[100px]" : "h-[50px]"
|
||||
}`}
|
||||
style={{ color: colors.text }}
|
||||
placeholder={selectedTypeConfig.placeholder}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={inputValue}
|
||||
onChangeText={setInputValue}
|
||||
keyboardType={selectedTypeConfig.keyboardType}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@@ -108,8 +109,25 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#F2F2F7",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
borderLight: isDark ? "#2C2C2E" : "#F3F4F6",
|
||||
pill: isDark ? "#4D4D4D" : "#E8E8ED",
|
||||
pillTransparent: isDark ? "rgba(56, 56, 58, 0.5)" : "rgba(232, 232, 237, 0.5)",
|
||||
inputBorder: isDark ? "rgba(56, 56, 58, 0.4)" : "rgba(209, 213, 219, 0.4)",
|
||||
inputBackground: isDark ? "rgba(28, 28, 30, 0.6)" : "rgba(255, 255, 255, 0.6)",
|
||||
selectedBg: isDark ? "#1C3A5F" : "#EBF4FF",
|
||||
accent: "#007AFF",
|
||||
chevron: isDark ? "#636366" : "#9CA3AF",
|
||||
};
|
||||
|
||||
const [selectedType, setSelectedType] = useState<LocationTypeId>("link");
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const [showTypePicker, setShowTypePicker] = useState(false);
|
||||
@@ -194,8 +212,11 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
|
||||
if (!booking) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-gray-500">No booking data</Text>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: transparentBackground ? "transparent" : colors.background }}
|
||||
>
|
||||
<Text style={{ color: colors.textSecondary }}>No booking data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -204,7 +225,8 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
<>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
className={`flex-1 ${backgroundStyle}`}
|
||||
className="flex-1"
|
||||
style={{ backgroundColor: transparentBackground ? "transparent" : colors.background }}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
@@ -217,15 +239,25 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
>
|
||||
{/* Current Location Info - hidden in transparent mode */}
|
||||
{!transparentBackground && booking.location && (
|
||||
<View className="mb-4 flex-row items-start rounded-xl bg-white p-4">
|
||||
<View
|
||||
className="mb-4 flex-row items-start rounded-xl p-4"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-10 w-10 items-center justify-center rounded-full ${pillStyle}`}
|
||||
className="mr-3 h-10 w-10 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: colors.pill }}
|
||||
>
|
||||
<Ionicons name="location" size={20} color="#6B7280" />
|
||||
<Ionicons name="location" size={20} color={colors.textSecondary} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[13px] font-medium text-gray-500">Current Location</Text>
|
||||
<Text className="mt-0.5 text-[15px] text-[#000]" numberOfLines={2}>
|
||||
<Text className="text-[13px] font-medium" style={{ color: colors.textSecondary }}>
|
||||
Current Location
|
||||
</Text>
|
||||
<Text
|
||||
className="mt-0.5 text-[15px]"
|
||||
style={{ color: colors.text }}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.location}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -234,57 +266,71 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
|
||||
{/* Location Type Selector */}
|
||||
{!transparentBackground && (
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-gray-500">
|
||||
<Text
|
||||
className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Location Type
|
||||
</Text>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
className={
|
||||
transparentBackground
|
||||
? "mb-4 flex-row items-center rounded-xl border border-gray-300/40 bg-white/60 px-4 py-3.5"
|
||||
: "mb-4 flex-row items-center rounded-xl bg-white px-4 py-3.5"
|
||||
}
|
||||
className="mb-4 flex-row items-center rounded-xl px-4 py-3.5"
|
||||
style={{
|
||||
backgroundColor: transparentBackground
|
||||
? colors.inputBackground
|
||||
: colors.cardBackground,
|
||||
borderWidth: transparentBackground ? 1 : 0,
|
||||
borderColor: colors.inputBorder,
|
||||
}}
|
||||
onPress={() => setShowTypePicker(true)}
|
||||
disabled={isSaving}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-10 w-10 items-center justify-center rounded-lg ${
|
||||
transparentBackground ? "bg-[#007AFF]/20" : "bg-[#007AFF]/15"
|
||||
}`}
|
||||
className="mr-3 h-10 w-10 items-center justify-center rounded-lg"
|
||||
style={{
|
||||
backgroundColor: isDark ? "rgba(0, 122, 255, 0.3)" : "rgba(0, 122, 255, 0.15)",
|
||||
}}
|
||||
>
|
||||
<Ionicons name={selectedTypeConfig.iconName} size={22} color="#007AFF" />
|
||||
<Ionicons name={selectedTypeConfig.iconName} size={22} color={colors.accent} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[17px] font-medium text-[#000]">
|
||||
<Text className="text-[17px] font-medium" style={{ color: colors.text }}>
|
||||
{selectedTypeConfig.label}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">
|
||||
<Text className="mt-0.5 text-[13px]" style={{ color: colors.textSecondary }}>
|
||||
{selectedTypeConfig.description}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-down" size={20} color="#9CA3AF" />
|
||||
<Ionicons name="chevron-down" size={20} color={colors.chevron} />
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* Location Input */}
|
||||
{!transparentBackground && (
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-gray-500">
|
||||
<Text
|
||||
className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
{selectedTypeConfig.label}
|
||||
</Text>
|
||||
)}
|
||||
<View
|
||||
className={
|
||||
transparentBackground
|
||||
? "mb-4 overflow-hidden rounded-xl border border-gray-300/40 bg-white/60"
|
||||
: "mb-4 overflow-hidden rounded-xl bg-white"
|
||||
}
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{
|
||||
backgroundColor: transparentBackground
|
||||
? colors.inputBackground
|
||||
: colors.cardBackground,
|
||||
borderWidth: transparentBackground ? 1 : 0,
|
||||
borderColor: colors.inputBorder,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
className={`px-4 py-3 text-[17px] text-[#000] ${
|
||||
className={`px-4 py-3 text-[17px] ${
|
||||
selectedTypeConfig.inputType === "multiline" ? "min-h-[100px]" : "h-[50px]"
|
||||
}`}
|
||||
style={{ color: colors.text }}
|
||||
placeholder={selectedTypeConfig.placeholder}
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={inputValue}
|
||||
onChangeText={setInputValue}
|
||||
keyboardType={selectedTypeConfig.keyboardType}
|
||||
@@ -312,10 +358,17 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
>
|
||||
<TouchableOpacity
|
||||
activeOpacity={1}
|
||||
className="max-h-[70%] w-[320px] overflow-hidden rounded-2xl bg-white"
|
||||
className="max-h-[70%] w-[320px] overflow-hidden rounded-2xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View className="border-b border-gray-200 px-4 py-3">
|
||||
<Text className="text-center text-[17px] font-semibold text-[#000]">
|
||||
<View
|
||||
className="px-4 py-3"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: colors.border }}
|
||||
>
|
||||
<Text
|
||||
className="text-center text-[17px] font-semibold"
|
||||
style={{ color: colors.text }}
|
||||
>
|
||||
Select Location Type
|
||||
</Text>
|
||||
</View>
|
||||
@@ -326,7 +379,8 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
const isSelected = item.id === selectedType;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center px-4 py-3.5 ${isSelected ? "bg-[#EBF4FF]" : ""}`}
|
||||
className="flex-row items-center px-4 py-3.5"
|
||||
style={{ backgroundColor: isSelected ? colors.selectedBg : "transparent" }}
|
||||
onPress={() => {
|
||||
setSelectedType(item.id);
|
||||
if (selectedType !== item.id) {
|
||||
@@ -336,36 +390,54 @@ export const EditLocationScreen = forwardRef<EditLocationScreenHandle, EditLocat
|
||||
}}
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-10 w-10 items-center justify-center rounded-lg ${
|
||||
isSelected ? "bg-[#007AFF]/20" : "bg-[#F2F2F7]"
|
||||
}`}
|
||||
className="mr-3 h-10 w-10 items-center justify-center rounded-lg"
|
||||
style={{
|
||||
backgroundColor: isSelected
|
||||
? isDark
|
||||
? "rgba(0, 122, 255, 0.3)"
|
||||
: "rgba(0, 122, 255, 0.2)"
|
||||
: colors.pill,
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name={item.iconName}
|
||||
size={22}
|
||||
color={isSelected ? "#007AFF" : "#6B7280"}
|
||||
color={isSelected ? colors.accent : colors.textSecondary}
|
||||
/>
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-semibold text-[#007AFF]" : "text-[#000]"
|
||||
}`}
|
||||
className="text-[17px]"
|
||||
style={{
|
||||
fontWeight: isSelected ? "600" : "400",
|
||||
color: isSelected ? colors.accent : colors.text,
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">{item.description}</Text>
|
||||
<Text
|
||||
className="mt-0.5 text-[13px]"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
{item.description}
|
||||
</Text>
|
||||
</View>
|
||||
{isSelected && <Ionicons name="checkmark" size={22} color="#007AFF" />}
|
||||
{isSelected && <Ionicons name="checkmark" size={22} color={colors.accent} />}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="border-t border-gray-200 py-3"
|
||||
className="py-3"
|
||||
style={{ borderTopWidth: 1, borderTopColor: colors.border }}
|
||||
onPress={() => setShowTypePicker(false)}
|
||||
>
|
||||
<Text className="text-center text-[17px] font-semibold text-[#007AFF]">Cancel</Text>
|
||||
<Text
|
||||
className="text-center text-[17px] font-semibold"
|
||||
style={{ color: colors.accent }}
|
||||
>
|
||||
Cancel
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -7,11 +7,20 @@
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useState } from "react";
|
||||
import { ActivityIndicator, Alert, FlatList, Text, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Alert,
|
||||
FlatList,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useMarkNoShow } from "@/hooks/useBookings";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { showErrorAlert, showSuccessAlert } from "@/utils/alerts";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
interface Attendee {
|
||||
id?: number | string;
|
||||
@@ -55,11 +64,26 @@ export function MarkNoShowScreen({
|
||||
transparentBackground = false,
|
||||
}: MarkNoShowScreenProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const backgroundColor = transparentBackground
|
||||
? "transparent"
|
||||
: isDark
|
||||
? theme.background
|
||||
: theme.backgroundMuted;
|
||||
const pillStyle = transparentBackground
|
||||
? "bg-[#E8E8ED]/50"
|
||||
: isDark
|
||||
? "bg-[#4D4D4D]"
|
||||
: "bg-[#E8E8ED]";
|
||||
const safeAttendees = Array.isArray(attendees) ? attendees : [];
|
||||
const [processingEmail, setProcessingEmail] = useState<string | null>(null);
|
||||
|
||||
// Dark mode colors
|
||||
const destructiveColor = theme.destructive;
|
||||
const successColor = "#16A34A";
|
||||
|
||||
// Mark no-show mutation
|
||||
const markNoShowMutation = useMarkNoShow();
|
||||
|
||||
@@ -156,18 +180,26 @@ export function MarkNoShowScreen({
|
||||
|
||||
// For transparent mode (iOS glass UI), use consistent styling regardless of no-show state
|
||||
const itemBgStyle = transparentBackground
|
||||
? "bg-white/60"
|
||||
? isDark
|
||||
? "bg-[#171717]/80"
|
||||
: "bg-white/60"
|
||||
: isNoShow
|
||||
? "bg-[#FEF2F2]"
|
||||
: "bg-white";
|
||||
? isDark
|
||||
? "bg-[#FF453A]/10"
|
||||
: "bg-[#FEF2F2]"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white";
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className={`flex-row items-center px-4 py-4 ${
|
||||
transparentBackground
|
||||
? `rounded-xl border border-gray-300/40 ${!isLast ? "mb-3" : ""}`
|
||||
? `rounded-xl ${isDark ? "border border-[#4D4D4D]/40" : "border border-gray-300/40"} ${!isLast ? "mb-3" : ""}`
|
||||
: !isLast
|
||||
? "border-b border-gray-100"
|
||||
? isDark
|
||||
? "border-b border-[#4D4D4D]"
|
||||
: "border-b border-gray-100"
|
||||
: ""
|
||||
} ${itemBgStyle}`}
|
||||
onPress={() => handleMarkNoShow(item)}
|
||||
@@ -176,46 +208,64 @@ export function MarkNoShowScreen({
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-12 w-12 items-center justify-center rounded-full ${
|
||||
transparentBackground ? "bg-[#000]" : isNoShow ? "bg-[#FECACA]" : pillStyle
|
||||
transparentBackground
|
||||
? "bg-[#000]"
|
||||
: isNoShow
|
||||
? isDark
|
||||
? "bg-[#FF453A]/20"
|
||||
: "bg-[#FECACA]"
|
||||
: pillStyle
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
className={`text-[16px] font-semibold ${
|
||||
transparentBackground ? "text-white" : isNoShow ? "text-[#800020]" : "text-gray-600"
|
||||
transparentBackground ? "text-white" : isDark ? "text-[#A3A3A3]" : "text-gray-600"
|
||||
}`}
|
||||
style={!transparentBackground && isNoShow ? { color: theme.destructive } : undefined}
|
||||
>
|
||||
{getInitials(item.name)}
|
||||
</Text>
|
||||
</View>
|
||||
<View className="mr-3 flex-1">
|
||||
<Text className="text-[17px] font-medium text-[#000]">{item.name || "Unknown"}</Text>
|
||||
<Text className="mt-0.5 text-[15px] text-gray-500">{item.email}</Text>
|
||||
<Text className={`text-[17px] font-medium ${isDark ? "text-white" : "text-[#000]"}`}>
|
||||
{item.name || "Unknown"}
|
||||
</Text>
|
||||
<Text className={`mt-0.5 text-[15px] ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}>
|
||||
{item.email}
|
||||
</Text>
|
||||
{isNoShow && (
|
||||
<View className="mt-1.5 flex-row items-center">
|
||||
<Ionicons name="eye-off" size={12} color="#800020" />
|
||||
<Text className="ml-1 text-[13px] font-medium text-[#800020]">Marked as no-show</Text>
|
||||
<Ionicons name="eye-off" size={12} color={destructiveColor} />
|
||||
<Text style={{ color: destructiveColor }} className="ml-1 text-[13px] font-medium">
|
||||
Marked as no-show
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{isProcessing ? (
|
||||
<ActivityIndicator size="small" color="#6B7280" />
|
||||
<ActivityIndicator size="small" color={isDark ? "#A3A3A3" : "#6B7280"} />
|
||||
) : (
|
||||
<View className="flex-row items-center">
|
||||
<View
|
||||
className={`flex-row items-center rounded-full px-3.5 py-2 ${
|
||||
isNoShow
|
||||
? "border border-[#16A34A] bg-white"
|
||||
: "border border-[#FEE2E2] bg-[#FEE2E2]"
|
||||
? isDark
|
||||
? "border border-[#16A34A] bg-[#171717]"
|
||||
: "border border-[#16A34A] bg-white"
|
||||
: isDark
|
||||
? "border border-[#FF453A]/30 bg-[#FF453A]/10"
|
||||
: "border border-[#FEE2E2] bg-[#FEE2E2]"
|
||||
}`}
|
||||
>
|
||||
<Ionicons
|
||||
name={isNoShow ? "eye" : "eye-off"}
|
||||
size={15}
|
||||
color={isNoShow ? "#16A34A" : "#800020"}
|
||||
color={isNoShow ? successColor : destructiveColor}
|
||||
style={{ marginRight: 5 }}
|
||||
/>
|
||||
<Text
|
||||
className={`text-[14px] font-semibold ${isNoShow ? "text-[#16A34A]" : "text-[#800020]"}`}
|
||||
style={{ color: isNoShow ? successColor : destructiveColor }}
|
||||
className="text-[14px] font-semibold"
|
||||
>
|
||||
{isNoShow ? "Unmark" : "Mark"}
|
||||
</Text>
|
||||
@@ -228,28 +278,36 @@ export function MarkNoShowScreen({
|
||||
|
||||
if (!booking) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-gray-500">No booking data</Text>
|
||||
<View className="flex-1 items-center justify-center" style={{ backgroundColor }}>
|
||||
<Text className={`${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}>No booking data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View className={`flex-1 ${backgroundStyle}`}>
|
||||
<View className="flex-1" style={{ backgroundColor }}>
|
||||
<View className="flex-1 p-4">
|
||||
{safeAttendees.length === 0 ? (
|
||||
<View className="flex-1 items-center justify-center">
|
||||
<View className="mb-4 h-20 w-20 items-center justify-center rounded-full bg-[#E8E8ED]">
|
||||
<Ionicons name="people" size={40} color="#9CA3AF" />
|
||||
<View
|
||||
className={`mb-4 h-20 w-20 items-center justify-center rounded-full ${pillStyle}`}
|
||||
>
|
||||
<Ionicons name="people" size={40} color={isDark ? "#A3A3A3" : "#9CA3AF"} />
|
||||
</View>
|
||||
<Text className="text-[17px] font-medium text-gray-700">No attendees found</Text>
|
||||
<Text className="mt-2 max-w-[280px] text-center text-[15px] text-gray-500">
|
||||
<Text className={`text-[17px] font-medium ${isDark ? "text-white" : "text-gray-700"}`}>
|
||||
No attendees found
|
||||
</Text>
|
||||
<Text
|
||||
className={`mt-2 max-w-[280px] text-center text-[15px] ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
Attendee information is not available for this booking.
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium uppercase tracking-wide text-gray-500">
|
||||
<Text
|
||||
className={`mb-2 px-1 text-[13px] font-medium uppercase tracking-wide ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
Attendees ({safeAttendees.length})
|
||||
</Text>
|
||||
{transparentBackground ? (
|
||||
@@ -261,7 +319,9 @@ export function MarkNoShowScreen({
|
||||
scrollEnabled={false}
|
||||
/>
|
||||
) : (
|
||||
<View className="overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className={`overflow-hidden rounded-xl ${isDark ? "bg-[#171717]" : "bg-white"}`}
|
||||
>
|
||||
<FlatList
|
||||
data={safeAttendees}
|
||||
renderItem={renderAttendee}
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
*/
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { FlatList, Text, View } from "react-native";
|
||||
import { FlatList, Text, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { ConferencingSession } from "@/services/types/bookings.types";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
export interface MeetingSessionDetailsScreenProps {
|
||||
sessions: ConferencingSession[];
|
||||
@@ -50,87 +51,125 @@ export function MeetingSessionDetailsScreen({
|
||||
transparentBackground = false,
|
||||
}: MeetingSessionDetailsScreenProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const backgroundStyle = transparentBackground
|
||||
? "bg-transparent"
|
||||
: isDark
|
||||
? "bg-black"
|
||||
: `bg-[${theme.backgroundMuted}]`;
|
||||
const pillStyle = transparentBackground
|
||||
? "bg-[#E8E8ED]/50"
|
||||
: isDark
|
||||
? "bg-[#4D4D4D]"
|
||||
: "bg-[#E8E8ED]";
|
||||
|
||||
const renderSession = ({ item, index }: { item: ConferencingSession; index: number }) => (
|
||||
<View
|
||||
className={`mb-4 overflow-hidden rounded-xl ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
>
|
||||
{/* Session header */}
|
||||
<View
|
||||
className={`flex-row items-center justify-between p-4 ${
|
||||
transparentBackground ? "" : "border-b border-gray-100"
|
||||
transparentBackground
|
||||
? ""
|
||||
: isDark
|
||||
? "border-b border-[#4D4D4D]"
|
||||
: "border-b border-gray-100"
|
||||
}`}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
<View className={`mr-3 h-10 w-10 items-center justify-center rounded-full ${pillStyle}`}>
|
||||
<Ionicons name="videocam" size={20} color="#6B7280" />
|
||||
<Ionicons name="videocam" size={20} color={isDark ? "#A3A3A3" : "#6B7280"} />
|
||||
</View>
|
||||
<Text className="text-[17px] font-semibold text-[#000]">
|
||||
<Text className={`text-[17px] font-semibold ${isDark ? "text-white" : "text-[#000]"}`}>
|
||||
Session {sessions.length > 1 ? index + 1 : ""}
|
||||
</Text>
|
||||
</View>
|
||||
{item.duration && (
|
||||
<View className={`rounded-full px-3 py-1 ${pillStyle}`}>
|
||||
<Text className="text-[13px] font-medium text-gray-600">
|
||||
<Text
|
||||
className={`text-[13px] font-medium ${isDark ? "text-[#A3A3A3]" : "text-gray-600"}`}
|
||||
>
|
||||
{formatDuration(item.duration)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Session details */}
|
||||
<View className="p-4">
|
||||
<View className="mb-2 flex-row items-center">
|
||||
<Ionicons name="time-outline" size={18} color="#8E8E93" />
|
||||
<Text className="ml-2 text-[15px] text-gray-600">
|
||||
<Ionicons name="time-outline" size={18} color="#A3A3A3" />
|
||||
<Text className={`ml-2 text-[15px] ${isDark ? "text-[#A3A3A3]" : "text-gray-600"}`}>
|
||||
Started: {formatDate(item.startTime)}
|
||||
</Text>
|
||||
</View>
|
||||
{item.endTime && (
|
||||
<View className="mb-2 flex-row items-center">
|
||||
<Ionicons name="time-outline" size={18} color="#8E8E93" />
|
||||
<Text className="ml-2 text-[15px] text-gray-600">
|
||||
<Ionicons name="time-outline" size={18} color="#A3A3A3" />
|
||||
<Text className={`ml-2 text-[15px] ${isDark ? "text-[#A3A3A3]" : "text-gray-600"}`}>
|
||||
Ended: {formatTime(item.endTime)}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{item.maxParticipants && (
|
||||
<View className="flex-row items-center">
|
||||
<Ionicons name="people-outline" size={18} color="#8E8E93" />
|
||||
<Text className="ml-2 text-[15px] text-gray-600">
|
||||
<Ionicons name="people-outline" size={18} color="#A3A3A3" />
|
||||
<Text className={`ml-2 text-[15px] ${isDark ? "text-[#A3A3A3]" : "text-gray-600"}`}>
|
||||
Max participants: {item.maxParticipants}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Participants */}
|
||||
{item.participants && item.participants.length > 0 && (
|
||||
<View className={`p-4 ${transparentBackground ? "" : "border-t border-gray-100"}`}>
|
||||
<Text className="mb-3 text-[13px] font-semibold uppercase tracking-wide text-gray-500">
|
||||
<View
|
||||
className={`p-4 ${
|
||||
transparentBackground
|
||||
? ""
|
||||
: isDark
|
||||
? "border-t border-[#4D4D4D]"
|
||||
: "border-t border-gray-100"
|
||||
}`}
|
||||
>
|
||||
<Text
|
||||
className={`mb-3 text-[13px] font-semibold uppercase tracking-wide ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
Participants ({item.participants.length})
|
||||
</Text>
|
||||
{item.participants.map((participant, pIndex) => (
|
||||
<View
|
||||
key={participant.id || pIndex}
|
||||
className={`mb-2 flex-row items-center p-3 ${
|
||||
transparentBackground ? "" : "rounded-xl bg-[#F2F2F7]"
|
||||
transparentBackground
|
||||
? ""
|
||||
: isDark
|
||||
? "rounded-xl bg-[#4D4D4D]/50"
|
||||
: "rounded-xl bg-[#F2F2F7]"
|
||||
}`}
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-9 w-9 items-center justify-center rounded-full ${pillStyle}`}
|
||||
>
|
||||
<Ionicons name="person" size={18} color="#6B7280" />
|
||||
<Ionicons name="person" size={18} color={isDark ? "#A3A3A3" : "#6B7280"} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[15px] font-medium text-[#000]">
|
||||
<Text
|
||||
className={`text-[15px] font-medium ${isDark ? "text-white" : "text-[#000]"}`}
|
||||
>
|
||||
{participant.userName || "Unknown participant"}
|
||||
</Text>
|
||||
<Text className="mt-0.5 text-[13px] text-gray-500">
|
||||
<Text
|
||||
className={`mt-0.5 text-[13px] ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
Joined: {formatTime(participant.joinTime)}
|
||||
{participant.duration && ` • ${formatDuration(participant.duration)}`}
|
||||
</Text>
|
||||
@@ -146,12 +185,16 @@ export function MeetingSessionDetailsScreen({
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center px-8 ${backgroundStyle}`}>
|
||||
<View className={`mb-4 h-16 w-16 items-center justify-center rounded-full ${pillStyle}`}>
|
||||
<Ionicons name="information-circle" size={32} color="#8E8E93" />
|
||||
<Ionicons name="information-circle" size={32} color="#A3A3A3" />
|
||||
</View>
|
||||
<Text className="text-center text-[17px] font-semibold text-[#000]">
|
||||
<Text
|
||||
className={`text-center text-[17px] font-semibold ${isDark ? "text-white" : "text-[#000]"}`}
|
||||
>
|
||||
No session details available
|
||||
</Text>
|
||||
<Text className="mt-2 text-center text-[15px] leading-5 text-gray-500">
|
||||
<Text
|
||||
className={`mt-2 text-center text-[15px] leading-5 ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
Session details may not be available for all meetings.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@@ -49,6 +50,22 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
function RescheduleScreen({ booking, onSuccess, onSavingChange, useNativeHeader = false }, ref) {
|
||||
const router = useRouter();
|
||||
const insets = useSafeAreaInsets();
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#F2F2F7",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
borderLight: isDark ? "#2C2C2E" : "#F3F4F6",
|
||||
pill: isDark ? "#4D4D4D" : "#E8E8ED",
|
||||
chevron: isDark ? "#636366" : "#C7C7CC",
|
||||
pressed: isDark ? "#2C2C2E" : "#F9F9F9",
|
||||
accent: "#007AFF",
|
||||
};
|
||||
|
||||
const [selectedDateTime, setSelectedDateTime] = useState<Date>(new Date());
|
||||
const [reason, setReason] = useState("");
|
||||
|
||||
@@ -153,24 +170,31 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
|
||||
if (!booking) {
|
||||
return (
|
||||
<View className="flex-1 items-center justify-center bg-[#F2F2F7]">
|
||||
<Text className="text-gray-500">No booking data</Text>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: colors.background }}
|
||||
>
|
||||
<Text style={{ color: colors.textSecondary }}>No booking data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView behavior="height" className="flex-1 bg-[#F2F2F7]">
|
||||
<KeyboardAvoidingView
|
||||
behavior="height"
|
||||
className="flex-1"
|
||||
style={{ backgroundColor: colors.background }}
|
||||
>
|
||||
{/* Header - only shown when not using native header */}
|
||||
{!useNativeHeader && (
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: "#fff",
|
||||
backgroundColor: colors.cardBackground,
|
||||
paddingTop: insets.top,
|
||||
paddingBottom: 12,
|
||||
paddingHorizontal: 16,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: "#E5E5EA",
|
||||
borderBottomColor: colors.border,
|
||||
elevation: 2,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
@@ -195,7 +219,7 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color="#000" />
|
||||
<Ionicons name="chevron-back" size={24} color={colors.text} />
|
||||
</AppPressable>
|
||||
|
||||
<Text
|
||||
@@ -204,7 +228,7 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
textAlign: "center",
|
||||
fontSize: 18,
|
||||
fontWeight: "600",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
marginHorizontal: 10,
|
||||
}}
|
||||
numberOfLines={1}
|
||||
@@ -225,7 +249,7 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
style={{
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
color: "#007AFF",
|
||||
color: colors.accent,
|
||||
}}
|
||||
>
|
||||
Save
|
||||
@@ -244,25 +268,42 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
keyboardShouldPersistTaps="handled"
|
||||
>
|
||||
{/* Booking Title Card */}
|
||||
<View className="mb-4 flex-row items-start rounded-xl bg-white p-4">
|
||||
<View className="mr-3 h-10 w-10 items-center justify-center rounded-full bg-[#E8E8ED]">
|
||||
<Ionicons name="calendar" size={20} color="#6B7280" />
|
||||
<View
|
||||
className="mb-4 flex-row items-start rounded-xl p-4"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-10 w-10 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: colors.pill }}
|
||||
>
|
||||
<Ionicons name="calendar" size={20} color={colors.textSecondary} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[13px] font-medium text-gray-500">Rescheduling</Text>
|
||||
<Text className="mt-0.5 text-[17px] font-medium text-[#000]" numberOfLines={2}>
|
||||
<Text className="text-[13px] font-medium" style={{ color: colors.textSecondary }}>
|
||||
Rescheduling
|
||||
</Text>
|
||||
<Text
|
||||
className="mt-0.5 text-[17px] font-medium"
|
||||
style={{ color: colors.text }}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Form Card */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
{/* Date picker trigger */}
|
||||
<Pressable
|
||||
className="flex-row items-center justify-between border-b border-gray-100 px-4 py-3"
|
||||
className="flex-row items-center justify-between px-4 py-3"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: pressed ? "#F9F9F9" : "transparent",
|
||||
backgroundColor: pressed ? colors.pressed : "transparent",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.borderLight,
|
||||
})}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening date picker");
|
||||
@@ -271,17 +312,26 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
disabled={isSaving}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="mb-1 text-[13px] font-medium text-gray-500">New Date</Text>
|
||||
<Text className="text-[17px] text-[#000]">{formattedDate}</Text>
|
||||
<Text
|
||||
className="mb-1 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
New Date
|
||||
</Text>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
{formattedDate}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</Pressable>
|
||||
|
||||
{/* Time picker trigger */}
|
||||
<Pressable
|
||||
className="flex-row items-center justify-between border-b border-gray-100 px-4 py-3"
|
||||
className="flex-row items-center justify-between px-4 py-3"
|
||||
style={({ pressed }) => ({
|
||||
backgroundColor: pressed ? "#F9F9F9" : "transparent",
|
||||
backgroundColor: pressed ? colors.pressed : "transparent",
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.borderLight,
|
||||
})}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening time picker");
|
||||
@@ -290,21 +340,32 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
disabled={isSaving}
|
||||
>
|
||||
<View className="flex-1">
|
||||
<Text className="mb-1 text-[13px] font-medium text-gray-500">New Time</Text>
|
||||
<Text className="text-[17px] text-[#000]">{formattedTime}</Text>
|
||||
<Text
|
||||
className="mb-1 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
New Time
|
||||
</Text>
|
||||
<Text className="text-[17px]" style={{ color: colors.text }}>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={18} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={18} color={colors.chevron} />
|
||||
</Pressable>
|
||||
|
||||
{/* Reason input */}
|
||||
<View className="px-4 py-3">
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">
|
||||
<Text
|
||||
className="mb-1.5 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Reason (optional)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="min-h-[80px] text-[17px] text-[#000]"
|
||||
className="min-h-[80px] text-[17px]"
|
||||
style={{ color: colors.text }}
|
||||
placeholder="Enter reason for rescheduling..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={reason}
|
||||
onChangeText={setReason}
|
||||
multiline
|
||||
|
||||
@@ -8,7 +8,14 @@
|
||||
import { DatePicker, Host } from "@expo/ui/swift-ui";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
import { KeyboardAvoidingView, ScrollView, Text, TextInput, View } from "react-native";
|
||||
import {
|
||||
KeyboardAvoidingView,
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useRescheduleBooking } from "@/hooks/useBookings";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
@@ -33,7 +40,21 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#F2F2F7",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
borderLight: isDark ? "#2C2C2E" : "#F3F4F6",
|
||||
pill: isDark ? "#4D4D4D" : "#E8E8ED",
|
||||
inputBorder: isDark ? "rgba(56, 56, 58, 0.4)" : "rgba(209, 213, 219, 0.4)",
|
||||
inputBackground: isDark ? "rgba(28, 28, 30, 0.6)" : "rgba(255, 255, 255, 0.6)",
|
||||
};
|
||||
|
||||
const [selectedDateTime, setSelectedDateTime] = useState<Date>(new Date());
|
||||
const [reason, setReason] = useState("");
|
||||
|
||||
@@ -113,14 +134,21 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
|
||||
if (!booking) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-gray-500">No booking data</Text>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: transparentBackground ? "transparent" : colors.background }}
|
||||
>
|
||||
<Text style={{ color: colors.textSecondary }}>No booking data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView behavior="padding" className={`flex-1 ${backgroundStyle}`}>
|
||||
<KeyboardAvoidingView
|
||||
behavior="padding"
|
||||
className="flex-1"
|
||||
style={{ backgroundColor: transparentBackground ? "transparent" : colors.background }}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
contentContainerStyle={{
|
||||
@@ -134,7 +162,8 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
<>
|
||||
{/* Booking Title */}
|
||||
<Text
|
||||
className="mb-4 mt-4 text-center text-[17px] font-semibold text-[#000]"
|
||||
className="mb-4 mt-4 text-center text-[17px] font-semibold"
|
||||
style={{ color: colors.text }}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.title}
|
||||
@@ -149,7 +178,9 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
selection={selectedDateTime}
|
||||
/>
|
||||
</Host>
|
||||
<Text className="mx-2 text-[15px] text-gray-500">at</Text>
|
||||
<Text className="mx-2 text-[15px]" style={{ color: colors.textSecondary }}>
|
||||
at
|
||||
</Text>
|
||||
<Host matchContents>
|
||||
<DatePicker
|
||||
onDateChange={handleTimeSelected}
|
||||
@@ -160,14 +191,25 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
</View>
|
||||
|
||||
{/* Reason input */}
|
||||
<Text className="mb-2 px-1 text-[13px] font-medium text-gray-500">
|
||||
<Text
|
||||
className="mb-2 px-1 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Reason (optional)
|
||||
</Text>
|
||||
<View className="mb-3 overflow-hidden rounded-xl border border-gray-300/40 bg-white/60 px-4 py-3">
|
||||
<View
|
||||
className="mb-3 overflow-hidden rounded-xl px-4 py-3"
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.inputBorder,
|
||||
backgroundColor: colors.inputBackground,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
className="min-h-[80px] text-[17px] text-[#000]"
|
||||
className="min-h-[80px] text-[17px]"
|
||||
style={{ color: colors.text }}
|
||||
placeholder="Enter reason for rescheduling..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={reason}
|
||||
onChangeText={setReason}
|
||||
multiline
|
||||
@@ -179,23 +221,46 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
) : (
|
||||
<>
|
||||
{/* Booking Title Card */}
|
||||
<View className="mb-4 flex-row items-start rounded-xl bg-white p-4">
|
||||
<View className="mr-3 h-10 w-10 items-center justify-center rounded-full bg-[#E8E8ED]">
|
||||
<Ionicons name="calendar" size={20} color="#6B7280" />
|
||||
<View
|
||||
className="mb-4 flex-row items-start rounded-xl p-4"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View
|
||||
className="mr-3 h-10 w-10 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: colors.pill }}
|
||||
>
|
||||
<Ionicons name="calendar" size={20} color={colors.textSecondary} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[13px] font-medium text-gray-500">Rescheduling</Text>
|
||||
<Text className="mt-0.5 text-[17px] font-medium text-[#000]" numberOfLines={2}>
|
||||
<Text className="text-[13px] font-medium" style={{ color: colors.textSecondary }}>
|
||||
Rescheduling
|
||||
</Text>
|
||||
<Text
|
||||
className="mt-0.5 text-[17px] font-medium"
|
||||
style={{ color: colors.text }}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.title}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* Form Card */}
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
{/* Date picker */}
|
||||
<View className="border-b border-gray-100 px-4 py-3">
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500">New Date</Text>
|
||||
<View
|
||||
className="px-4 py-3"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: colors.borderLight }}
|
||||
>
|
||||
<Text
|
||||
className="mb-2 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
New Date
|
||||
</Text>
|
||||
<Host matchContents>
|
||||
<DatePicker
|
||||
onDateChange={handleDateSelected}
|
||||
@@ -206,8 +271,16 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
</View>
|
||||
|
||||
{/* Time picker */}
|
||||
<View className="border-b border-gray-100 px-4 py-3">
|
||||
<Text className="mb-2 text-[13px] font-medium text-gray-500">New Time</Text>
|
||||
<View
|
||||
className="px-4 py-3"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: colors.borderLight }}
|
||||
>
|
||||
<Text
|
||||
className="mb-2 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
New Time
|
||||
</Text>
|
||||
<Host matchContents>
|
||||
<DatePicker
|
||||
onDateChange={handleTimeSelected}
|
||||
@@ -219,13 +292,17 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
|
||||
{/* Reason input */}
|
||||
<View className="px-4 py-3">
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">
|
||||
<Text
|
||||
className="mb-1.5 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Reason (optional)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="min-h-[80px] text-[17px] text-[#000]"
|
||||
className="min-h-[80px] text-[17px]"
|
||||
style={{ color: colors.text }}
|
||||
placeholder="Enter reason for rescheduling..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={reason}
|
||||
onChangeText={setReason}
|
||||
multiline
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableOpacity,
|
||||
useColorScheme,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
@@ -46,8 +47,24 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
ref
|
||||
) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
|
||||
const colors = {
|
||||
background: isDark ? "#000000" : "#F2F2F7",
|
||||
cardBackground: isDark ? "#171717" : "#FFFFFF",
|
||||
text: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#A3A3A3" : "#6B7280",
|
||||
border: isDark ? "#4D4D4D" : "#E5E5EA",
|
||||
borderLight: isDark ? "#2C2C2E" : "#F3F4F6",
|
||||
pill: isDark ? "#4D4D4D" : "#E8E8ED",
|
||||
pillTransparent: isDark ? "rgba(56, 56, 58, 0.5)" : "rgba(232, 232, 237, 0.5)",
|
||||
inputBorder: isDark ? "rgba(56, 56, 58, 0.4)" : "rgba(209, 213, 219, 0.4)",
|
||||
inputBackground: isDark ? "rgba(28, 28, 30, 0.6)" : "rgba(255, 255, 255, 0.6)",
|
||||
selectedBg: isDark ? "#1C3A5F" : "#E8F4FD",
|
||||
accent: "#007AFF",
|
||||
};
|
||||
|
||||
const [selectedDateTime, setSelectedDateTime] = useState<Date>(new Date());
|
||||
const [showDatePicker, setShowDatePicker] = useState(false);
|
||||
const [showTimePicker, setShowTimePicker] = useState(false);
|
||||
@@ -182,8 +199,11 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
|
||||
if (!booking) {
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center ${backgroundStyle}`}>
|
||||
<Text className="text-gray-500">No booking data</Text>
|
||||
<View
|
||||
className="flex-1 items-center justify-center"
|
||||
style={{ backgroundColor: transparentBackground ? "transparent" : colors.background }}
|
||||
>
|
||||
<Text style={{ color: colors.textSecondary }}>No booking data</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -192,7 +212,8 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
<>
|
||||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
||||
className={`flex-1 ${backgroundStyle}`}
|
||||
className="flex-1"
|
||||
style={{ backgroundColor: transparentBackground ? "transparent" : colors.background }}
|
||||
>
|
||||
<ScrollView
|
||||
className="flex-1"
|
||||
@@ -205,15 +226,25 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
>
|
||||
{/* Booking Title Card */}
|
||||
{!transparentBackground && (
|
||||
<View className="mb-4 flex-row items-start rounded-xl bg-white p-4">
|
||||
<View
|
||||
className="mb-4 flex-row items-start rounded-xl p-4"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<View
|
||||
className={`mr-3 h-10 w-10 items-center justify-center rounded-full ${pillStyle}`}
|
||||
className="mr-3 h-10 w-10 items-center justify-center rounded-full"
|
||||
style={{ backgroundColor: colors.pill }}
|
||||
>
|
||||
<Ionicons name="calendar" size={20} color="#6B7280" />
|
||||
<Ionicons name="calendar" size={20} color={colors.textSecondary} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[13px] font-medium text-gray-500">Rescheduling</Text>
|
||||
<Text className="mt-0.5 text-[17px] font-medium text-[#000]" numberOfLines={2}>
|
||||
<Text className="text-[13px] font-medium" style={{ color: colors.textSecondary }}>
|
||||
Rescheduling
|
||||
</Text>
|
||||
<Text
|
||||
className="mt-0.5 text-[17px] font-medium"
|
||||
style={{ color: colors.text }}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.title}
|
||||
</Text>
|
||||
</View>
|
||||
@@ -226,9 +257,19 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
{/* Date picker - pill button */}
|
||||
{!isWeb && (
|
||||
<View className="mb-3 flex-row items-center">
|
||||
<Text className="mr-3 text-[15px] font-medium text-gray-600">Date</Text>
|
||||
<Text
|
||||
className="mr-3 text-[15px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Date
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
className="rounded-xl border border-gray-300/40 bg-white/60 px-4 py-2.5"
|
||||
className="rounded-xl px-4 py-2.5"
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.inputBorder,
|
||||
backgroundColor: colors.inputBackground,
|
||||
}}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening date picker");
|
||||
setShowDatePicker(true);
|
||||
@@ -236,7 +277,9 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
disabled={isSaving}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text className="text-[17px] font-medium text-[#000]">{formattedDate}</Text>
|
||||
<Text className="text-[17px] font-medium" style={{ color: colors.text }}>
|
||||
{formattedDate}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
@@ -244,9 +287,19 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
{/* Time picker - pill button */}
|
||||
{!isWeb && (
|
||||
<View className="mb-3 flex-row items-center">
|
||||
<Text className="mr-3 text-[15px] font-medium text-gray-600">Time</Text>
|
||||
<Text
|
||||
className="mr-3 text-[15px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Time
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
className="rounded-xl border border-gray-300/40 bg-white/60 px-4 py-2.5"
|
||||
className="rounded-xl px-4 py-2.5"
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.inputBorder,
|
||||
backgroundColor: colors.inputBackground,
|
||||
}}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening time picker");
|
||||
setShowTimePicker(true);
|
||||
@@ -254,17 +307,27 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
disabled={isSaving}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text className="text-[17px] font-medium text-[#000]">{formattedTime}</Text>
|
||||
<Text className="text-[17px] font-medium" style={{ color: colors.text }}>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Reason input */}
|
||||
<View className="mb-3 overflow-hidden rounded-xl border border-gray-300/40 bg-white/60 px-4 py-3">
|
||||
<View
|
||||
className="mb-3 overflow-hidden rounded-xl px-4 py-3"
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: colors.inputBorder,
|
||||
backgroundColor: colors.inputBackground,
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
className="min-h-[80px] text-[17px] text-[#000]"
|
||||
className="min-h-[80px] text-[17px]"
|
||||
style={{ color: colors.text }}
|
||||
placeholder="Reason for rescheduling (optional)..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={reason}
|
||||
onChangeText={setReason}
|
||||
multiline
|
||||
@@ -274,11 +337,22 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
</View>
|
||||
</>
|
||||
) : (
|
||||
<View className="mb-4 overflow-hidden rounded-xl bg-white">
|
||||
<View
|
||||
className="mb-4 overflow-hidden rounded-xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
{/* Date picker */}
|
||||
{isWeb ? (
|
||||
<View className="border-b border-gray-100 px-4 py-3">
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">New Date</Text>
|
||||
<View
|
||||
className="px-4 py-3"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: colors.borderLight }}
|
||||
>
|
||||
<Text
|
||||
className="mb-1.5 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
New Date
|
||||
</Text>
|
||||
<input
|
||||
type="date"
|
||||
value={formatLocalDate(selectedDateTime)}
|
||||
@@ -299,16 +373,25 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
border: "none",
|
||||
outline: "none",
|
||||
backgroundColor: "transparent",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
}}
|
||||
min={formatLocalDate(new Date())}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View className="border-b border-gray-100 px-4 py-3">
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">New Date</Text>
|
||||
<View
|
||||
className="px-4 py-3"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: colors.borderLight }}
|
||||
>
|
||||
<Text
|
||||
className="mb-1.5 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
New Date
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
className={`self-start rounded-lg px-4 py-2 ${pillStyle}`}
|
||||
className="self-start rounded-lg px-4 py-2"
|
||||
style={{ backgroundColor: colors.pill }}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening date picker");
|
||||
setShowDatePicker(true);
|
||||
@@ -316,15 +399,25 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
disabled={isSaving}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text className="text-[17px] font-medium text-[#000]">{formattedDate}</Text>
|
||||
<Text className="text-[17px] font-medium" style={{ color: colors.text }}>
|
||||
{formattedDate}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Time picker */}
|
||||
{isWeb ? (
|
||||
<View className="border-b border-gray-100 px-4 py-3">
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">New Time</Text>
|
||||
<View
|
||||
className="px-4 py-3"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: colors.borderLight }}
|
||||
>
|
||||
<Text
|
||||
className="mb-1.5 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
New Time
|
||||
</Text>
|
||||
<input
|
||||
type="time"
|
||||
value={`${String(selectedDateTime.getHours()).padStart(2, "0")}:${String(
|
||||
@@ -351,15 +444,24 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
border: "none",
|
||||
outline: "none",
|
||||
backgroundColor: "transparent",
|
||||
color: "#000",
|
||||
color: colors.text,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<View className="border-b border-gray-100 px-4 py-3">
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">New Time</Text>
|
||||
<View
|
||||
className="px-4 py-3"
|
||||
style={{ borderBottomWidth: 1, borderBottomColor: colors.borderLight }}
|
||||
>
|
||||
<Text
|
||||
className="mb-1.5 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
New Time
|
||||
</Text>
|
||||
<TouchableOpacity
|
||||
className={`self-start rounded-lg px-4 py-2 ${pillStyle}`}
|
||||
className="self-start rounded-lg px-4 py-2"
|
||||
style={{ backgroundColor: colors.pill }}
|
||||
onPress={() => {
|
||||
safeLogInfo("[RescheduleScreen] Opening time picker");
|
||||
setShowTimePicker(true);
|
||||
@@ -367,20 +469,26 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
disabled={isSaving}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text className="text-[17px] font-medium text-[#000]">{formattedTime}</Text>
|
||||
<Text className="text-[17px] font-medium" style={{ color: colors.text }}>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Reason input */}
|
||||
<View className="px-4 py-3">
|
||||
<Text className="mb-1.5 text-[13px] font-medium text-gray-500">
|
||||
<Text
|
||||
className="mb-1.5 text-[13px] font-medium"
|
||||
style={{ color: colors.textSecondary }}
|
||||
>
|
||||
Reason (optional)
|
||||
</Text>
|
||||
<TextInput
|
||||
className="min-h-[80px] text-[17px] text-[#000]"
|
||||
className="min-h-[80px] text-[17px]"
|
||||
style={{ color: colors.text }}
|
||||
placeholder="Enter reason for rescheduling..."
|
||||
placeholderTextColor="#9CA3AF"
|
||||
placeholderTextColor={colors.textSecondary}
|
||||
value={reason}
|
||||
onChangeText={setReason}
|
||||
multiline
|
||||
@@ -406,8 +514,18 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
onPress={() => setShowDatePicker(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1}>
|
||||
<View className="w-[320px] overflow-hidden rounded-2xl bg-white">
|
||||
<Text className="border-b border-gray-200 py-3 text-center text-[17px] font-semibold text-[#000]">
|
||||
<View
|
||||
className="w-[320px] overflow-hidden rounded-2xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<Text
|
||||
className="py-3 text-center text-[17px] font-semibold"
|
||||
style={{
|
||||
color: colors.text,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
}}
|
||||
>
|
||||
Select Date
|
||||
</Text>
|
||||
<ScrollView
|
||||
@@ -421,7 +539,8 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.value.toISOString()}
|
||||
className={`px-4 py-3 ${isSelected ? "bg-[#E8F4FD]" : ""}`}
|
||||
className="px-4 py-3"
|
||||
style={{ backgroundColor: isSelected ? colors.selectedBg : "transparent" }}
|
||||
onPress={() => {
|
||||
const newDate = new Date(option.value);
|
||||
newDate.setHours(selectedDateTime.getHours());
|
||||
@@ -432,23 +551,33 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-semibold text-[#007AFF]" : "text-[#000]"
|
||||
}`}
|
||||
className="text-[17px]"
|
||||
style={{
|
||||
fontWeight: isSelected ? "600" : "400",
|
||||
color: isSelected ? colors.accent : colors.text,
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
{isSelected && <Ionicons name="checkmark" size={22} color="#007AFF" />}
|
||||
{isSelected && (
|
||||
<Ionicons name="checkmark" size={22} color={colors.accent} />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
<TouchableOpacity
|
||||
className="border-t border-gray-200 py-3"
|
||||
className="py-3"
|
||||
style={{ borderTopWidth: 1, borderTopColor: colors.border }}
|
||||
onPress={() => setShowDatePicker(false)}
|
||||
>
|
||||
<Text className="text-center text-[17px] font-semibold text-[#007AFF]">Done</Text>
|
||||
<Text
|
||||
className="text-center text-[17px] font-semibold"
|
||||
style={{ color: colors.accent }}
|
||||
>
|
||||
Done
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
@@ -468,8 +597,18 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
onPress={() => setShowTimePicker(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1}>
|
||||
<View className="w-[320px] overflow-hidden rounded-2xl bg-white">
|
||||
<Text className="border-b border-gray-200 py-3 text-center text-[17px] font-semibold text-[#000]">
|
||||
<View
|
||||
className="w-[320px] overflow-hidden rounded-2xl"
|
||||
style={{ backgroundColor: colors.cardBackground }}
|
||||
>
|
||||
<Text
|
||||
className="py-3 text-center text-[17px] font-semibold"
|
||||
style={{
|
||||
color: colors.text,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.border,
|
||||
}}
|
||||
>
|
||||
Select Time
|
||||
</Text>
|
||||
<ScrollView
|
||||
@@ -484,7 +623,8 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
return (
|
||||
<TouchableOpacity
|
||||
key={option.label}
|
||||
className={`px-4 py-3 ${isSelected ? "bg-[#E8F4FD]" : ""}`}
|
||||
className="px-4 py-3"
|
||||
style={{ backgroundColor: isSelected ? colors.selectedBg : "transparent" }}
|
||||
onPress={() => {
|
||||
const newDate = new Date(selectedDateTime);
|
||||
newDate.setHours(option.value.hour);
|
||||
@@ -495,23 +635,33 @@ export const RescheduleScreen = forwardRef<RescheduleScreenHandle, RescheduleScr
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<Text
|
||||
className={`text-[17px] ${
|
||||
isSelected ? "font-semibold text-[#007AFF]" : "text-[#000]"
|
||||
}`}
|
||||
className="text-[17px]"
|
||||
style={{
|
||||
fontWeight: isSelected ? "600" : "400",
|
||||
color: isSelected ? colors.accent : colors.text,
|
||||
}}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
{isSelected && <Ionicons name="checkmark" size={22} color="#007AFF" />}
|
||||
{isSelected && (
|
||||
<Ionicons name="checkmark" size={22} color={colors.accent} />
|
||||
)}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
<TouchableOpacity
|
||||
className="border-t border-gray-200 py-3"
|
||||
className="py-3"
|
||||
style={{ borderTopWidth: 1, borderTopColor: colors.border }}
|
||||
onPress={() => setShowTimePicker(false)}
|
||||
>
|
||||
<Text className="text-center text-[17px] font-semibold text-[#007AFF]">Done</Text>
|
||||
<Text
|
||||
className="text-center text-[17px] font-semibold"
|
||||
style={{ color: colors.accent }}
|
||||
>
|
||||
Done
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import { FlatList, Text, TouchableOpacity, View } from "react-native";
|
||||
import { FlatList, Text, TouchableOpacity, useColorScheme, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { BookingRecording } from "@/services/types/bookings.types";
|
||||
import { getColors } from "@/constants/colors";
|
||||
|
||||
export interface ViewRecordingsScreenProps {
|
||||
recordings: BookingRecording[];
|
||||
@@ -40,8 +41,19 @@ export function ViewRecordingsScreen({
|
||||
transparentBackground = false,
|
||||
}: ViewRecordingsScreenProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const backgroundStyle = transparentBackground ? "bg-transparent" : "bg-[#F2F2F7]";
|
||||
const pillStyle = transparentBackground ? "bg-[#E8E8ED]/50" : "bg-[#E8E8ED]";
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const theme = getColors(isDark);
|
||||
const backgroundStyle = transparentBackground
|
||||
? "bg-transparent"
|
||||
: isDark
|
||||
? "bg-black"
|
||||
: `bg-[${theme.backgroundMuted}]`;
|
||||
const pillStyle = transparentBackground
|
||||
? "bg-[#E8E8ED]/50"
|
||||
: isDark
|
||||
? "bg-[#4D4D4D]"
|
||||
: "bg-[#E8E8ED]";
|
||||
|
||||
const handleOpenRecording = async (recording: BookingRecording) => {
|
||||
try {
|
||||
@@ -56,25 +68,35 @@ export function ViewRecordingsScreen({
|
||||
const renderRecording = ({ item }: { item: BookingRecording }) => (
|
||||
<TouchableOpacity
|
||||
className={`mb-3 overflow-hidden rounded-xl ${
|
||||
transparentBackground ? "border border-gray-300/40 bg-white/60" : "bg-white"
|
||||
transparentBackground
|
||||
? isDark
|
||||
? "border border-[#4D4D4D]/40 bg-[#171717]/80"
|
||||
: "border border-gray-300/40 bg-white/60"
|
||||
: isDark
|
||||
? "bg-[#171717]"
|
||||
: "bg-white"
|
||||
}`}
|
||||
onPress={() => handleOpenRecording(item)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View className="flex-row items-center p-4">
|
||||
<View className={`mr-3 h-10 w-10 items-center justify-center rounded-full ${pillStyle}`}>
|
||||
<Ionicons name="videocam" size={20} color="#6B7280" />
|
||||
<Ionicons name="videocam" size={20} color={isDark ? "#A3A3A3" : "#6B7280"} />
|
||||
</View>
|
||||
<View className="flex-1">
|
||||
<Text className="text-[17px] font-medium text-[#000]">Recording</Text>
|
||||
<Text className="mt-0.5 text-[15px] text-gray-500">{formatDate(item.startTime)}</Text>
|
||||
<Text className={`text-[17px] font-medium ${isDark ? "text-white" : "text-[#000]"}`}>
|
||||
Recording
|
||||
</Text>
|
||||
<Text className={`mt-0.5 text-[15px] ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}>
|
||||
{formatDate(item.startTime)}
|
||||
</Text>
|
||||
{item.duration && (
|
||||
<Text className="mt-0.5 text-[13px] text-gray-400">
|
||||
<Text className={`mt-0.5 text-[13px] ${isDark ? "text-[#636366]" : "text-gray-400"}`}>
|
||||
Duration: {formatDuration(item.duration)}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
<Ionicons name="chevron-forward" size={20} color="#C7C7CC" />
|
||||
<Ionicons name="chevron-forward" size={20} color={isDark ? "#48484A" : "#C7C7CC"} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
@@ -83,12 +105,16 @@ export function ViewRecordingsScreen({
|
||||
return (
|
||||
<View className={`flex-1 items-center justify-center px-8 ${backgroundStyle}`}>
|
||||
<View className={`mb-4 h-16 w-16 items-center justify-center rounded-full ${pillStyle}`}>
|
||||
<Ionicons name="videocam-off" size={32} color="#8E8E93" />
|
||||
<Ionicons name="videocam-off" size={32} color="#A3A3A3" />
|
||||
</View>
|
||||
<Text className="text-center text-[17px] font-semibold text-[#000]">
|
||||
<Text
|
||||
className={`text-center text-[17px] font-semibold ${isDark ? "text-white" : "text-[#000]"}`}
|
||||
>
|
||||
No recordings available
|
||||
</Text>
|
||||
<Text className="mt-2 text-center text-[15px] leading-5 text-gray-500">
|
||||
<Text
|
||||
className={`mt-2 text-center text-[15px] leading-5 ${isDark ? "text-[#A3A3A3]" : "text-gray-500"}`}
|
||||
>
|
||||
Recordings may take some time to process after the meeting ends.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { semanticColors } from "@/constants/colors";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { View } from "react-native";
|
||||
import { useColorScheme, View } from "react-native";
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: React.ComponentProps<typeof View> & React.RefAttributes<View>) {
|
||||
const colorScheme = useColorScheme();
|
||||
const isDark = colorScheme === "dark";
|
||||
const backgroundColor = isDark ? semanticColors.skeleton.dark : semanticColors.skeleton.light;
|
||||
|
||||
return (
|
||||
<View
|
||||
className={cn("rounded-md", className)}
|
||||
style={[{ backgroundColor: "#E5E5EA" }, style]}
|
||||
{...props}
|
||||
/>
|
||||
<View className={cn("rounded-md", className)} style={[{ backgroundColor }, style]} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ export function Toast({ visible, message, type }: ToastProps) {
|
||||
>
|
||||
<View
|
||||
className={`flex-row items-center rounded-lg px-4 py-3 shadow-lg ${
|
||||
type === "error" ? "bg-red-600" : "bg-gray-800"
|
||||
type === "error" ? "bg-red-600" : "bg-gray-800 dark:bg-[#262626]"
|
||||
}`}
|
||||
>
|
||||
<Ionicons
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* Centralized color definitions for the Cal.com Companion app.
|
||||
*
|
||||
* These colors are aligned with the main Cal.com website's design system.
|
||||
* Use these constants for inline styles. For Tailwind classes, use the
|
||||
* corresponding cal-* color classes defined in tailwind.config.js.
|
||||
*
|
||||
* Color Philosophy:
|
||||
* - Light mode: Clean whites and light grays
|
||||
* - Dark mode: Pure black (#000000) for OLED efficiency, with neutral grays
|
||||
* from the main website (#171717, #262626, #404040) for secondary elements
|
||||
*/
|
||||
|
||||
export const colors = {
|
||||
light: {
|
||||
// Backgrounds
|
||||
background: "#FFFFFF",
|
||||
backgroundSecondary: "#F8F9FA",
|
||||
backgroundMuted: "#F2F2F7",
|
||||
backgroundEmphasis: "#E5E5EA",
|
||||
|
||||
// Text
|
||||
text: "#333333",
|
||||
textSecondary: "#666666",
|
||||
textMuted: "#A3A3A3",
|
||||
textEmphasis: "#3C3F44",
|
||||
|
||||
// Borders
|
||||
border: "#E5E5EA",
|
||||
borderLight: "#C6C6C8",
|
||||
borderSubtle: "#D1D5DB",
|
||||
|
||||
// Accent colors
|
||||
accent: "#007AFF",
|
||||
success: "#34C759",
|
||||
warning: "#FF9500",
|
||||
error: "#FF3B30",
|
||||
destructive: "#800020",
|
||||
},
|
||||
|
||||
dark: {
|
||||
// Backgrounds - Pure black for main, website grays for secondary
|
||||
background: "#000000",
|
||||
backgroundSecondary: "#171717",
|
||||
backgroundMuted: "#262626",
|
||||
backgroundEmphasis: "#404040",
|
||||
|
||||
// Text - White primary, website gray for secondary
|
||||
text: "#FFFFFF",
|
||||
textSecondary: "#A3A3A3",
|
||||
textMuted: "#A3A3A3",
|
||||
|
||||
// Borders - Website grays
|
||||
border: "#4D4D4D",
|
||||
borderLight: "#4D4D4D",
|
||||
borderSubtle: "#262626",
|
||||
|
||||
// Accent colors
|
||||
accent: "#007AFF",
|
||||
success: "#34C759",
|
||||
warning: "#FF9500",
|
||||
error: "#FF3B30",
|
||||
destructive: "#FF453A",
|
||||
},
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Helper function to get colors based on dark mode state.
|
||||
* @param isDark - Whether dark mode is enabled
|
||||
* @returns The appropriate color palette
|
||||
*/
|
||||
export function getColors(isDark: boolean) {
|
||||
return isDark ? colors.dark : colors.light;
|
||||
}
|
||||
|
||||
/**
|
||||
* Semantic color mappings for common UI elements.
|
||||
* These provide consistent styling across the app.
|
||||
*/
|
||||
export const semanticColors = {
|
||||
// Card backgrounds
|
||||
card: {
|
||||
light: colors.light.background,
|
||||
dark: colors.dark.backgroundSecondary,
|
||||
},
|
||||
|
||||
// Input field backgrounds
|
||||
input: {
|
||||
light: colors.light.background,
|
||||
dark: colors.dark.backgroundSecondary,
|
||||
},
|
||||
|
||||
// Modal/sheet backgrounds
|
||||
modal: {
|
||||
light: colors.light.background,
|
||||
dark: colors.dark.background,
|
||||
},
|
||||
|
||||
// List item backgrounds
|
||||
listItem: {
|
||||
light: colors.light.background,
|
||||
dark: colors.dark.backgroundSecondary,
|
||||
},
|
||||
|
||||
// Transparent background mode (glass UI effect)
|
||||
transparentBg: {
|
||||
light: "rgba(255, 255, 255, 0.6)",
|
||||
dark: "rgba(23, 23, 23, 0.8)",
|
||||
},
|
||||
|
||||
transparentBorder: {
|
||||
light: "rgba(209, 213, 219, 0.4)",
|
||||
dark: "rgba(77, 77, 77, 0.4)",
|
||||
},
|
||||
|
||||
// Skeleton loading state
|
||||
skeleton: {
|
||||
light: colors.light.backgroundEmphasis,
|
||||
dark: colors.dark.backgroundEmphasis,
|
||||
},
|
||||
} as const;
|
||||
+22
-20
@@ -26,26 +26,28 @@
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 350 100% 15%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 350 100% 15%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
}
|
||||
}
|
||||
|
||||
input:focus {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user