feat(companion): component improvements, migrate to path aliases, expo-haptics, expo-image (#26226)
* react compiler * remove compilation mode 'all', it will use infer by default * feat(companion): add theme tokens, path aliases, and component improvements - Extend tailwind.config.js with Cal.com brand color tokens (cal.text, cal.bg, cal.border, cal.accent, cal.brand) - Create theme/colors.ts for JS usage where Tailwind classes can't be used - Configure path aliases in tsconfig.json (@/components, @/hooks, @/utils, etc.) - Add expo-image and expo-haptics dependencies - Enhance AppPressable with Reanimated animations (opacity + scale) and haptic feedback - Extract shared logic from BookingListItem into useBookingListItemData hook and BookingListItemParts - Extract shared logic from EventTypeListItem into useEventTypeListItemData hook and EventTypeListItemParts - Extract shared logic from AvailabilityListItem into AvailabilityListItemParts - Update all list item components to use new theme tokens instead of hardcoded hex values * fix react compiler non memo issue * feat(companion): migrate to path aliases and expo-image - Remove theme/colors.ts and theme/index.ts (defer dark mode to future PR) - Replace colors import with hardcoded hex values in components - Migrate all files to use path aliases (@/components/*, @/hooks/*, etc.) - Migrate SvgImage to use expo-image for better performance - Update tsconfig.json to remove @/theme/* alias * fix(companion): add path aliases to extension tsconfig Add baseUrl and paths configuration to tsconfig.extension.json to support path aliases in shared types used by the extension build.
This commit is contained in:
1 parent
b6b0492362
commit
9203cb262c
81 files changed
+1002
-671
No files matched your search
@@ -2,7 +2,7 @@ import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { AvailabilityListScreen } from "../../../components/screens/AvailabilityListScreen";
|
||||
import { AvailabilityListScreen } from "@/components/screens/AvailabilityListScreen";
|
||||
|
||||
export default function Availability() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Platform, Text, View } from "react-native";
|
||||
import { AppPressable } from "../../../components/AppPressable";
|
||||
import type { AddGuestsScreenHandle } from "../../../components/screens/AddGuestsScreen";
|
||||
import AddGuestsScreenComponent from "../../../components/screens/AddGuestsScreen";
|
||||
import { type Booking, CalComAPIService } from "../../../services/calcom";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import type { AddGuestsScreenHandle } from "@/components/screens/AddGuestsScreen";
|
||||
import AddGuestsScreenComponent from "@/components/screens/AddGuestsScreen";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
|
||||
export default function AddGuests() {
|
||||
const { uid } = useLocalSearchParams<{ uid: string }>();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import { BookingDetailScreen } from "../../../components/screens/BookingDetailScreen";
|
||||
import { useAuth } from "../../../contexts/AuthContext";
|
||||
import { type Booking, CalComAPIService } from "../../../services/calcom";
|
||||
import { type BookingActionsResult, getBookingActions } from "../../../utils/booking-actions";
|
||||
import { BookingDetailScreen } from "@/components/screens/BookingDetailScreen";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
|
||||
|
||||
// Empty actions result for when no booking is loaded
|
||||
const EMPTY_ACTIONS: BookingActionsResult = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Platform, Text, View } from "react-native";
|
||||
import { AppPressable } from "../../../components/AppPressable";
|
||||
import type { EditLocationScreenHandle } from "../../../components/screens/EditLocationScreen";
|
||||
import EditLocationScreenComponent from "../../../components/screens/EditLocationScreen";
|
||||
import { type Booking, CalComAPIService } from "../../../services/calcom";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import type { EditLocationScreenHandle } from "@/components/screens/EditLocationScreen";
|
||||
import EditLocationScreenComponent from "@/components/screens/EditLocationScreen";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
|
||||
export default function EditLocation() {
|
||||
const { uid } = useLocalSearchParams<{ uid: string }>();
|
||||
|
||||
@@ -3,9 +3,9 @@ import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Stack } from "expo-router";
|
||||
import { useState } from "react";
|
||||
|
||||
import { BookingListScreen } from "../../../components/booking-list-screen/BookingListScreen";
|
||||
import { useEventTypes } from "../../../hooks";
|
||||
import { useActiveBookingFilter } from "../../../hooks/useActiveBookingFilter";
|
||||
import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen";
|
||||
import { useEventTypes } from "@/hooks";
|
||||
import { useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
|
||||
|
||||
export default function Bookings() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -3,11 +3,11 @@ import SegmentedControl from "@react-native-segmented-control/segmented-control"
|
||||
import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { useState } from "react";
|
||||
import { Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { BookingListScreen } from "../../../components/booking-list-screen/BookingListScreen";
|
||||
import { Header } from "../../../components/Header";
|
||||
import { useActiveBookingFilter } from "../../../hooks/useActiveBookingFilter";
|
||||
import type { EventType } from "../../../services/calcom";
|
||||
import { CalComAPIService } from "../../../services/calcom";
|
||||
import { BookingListScreen } from "@/components/booking-list-screen/BookingListScreen";
|
||||
import { Header } from "@/components/Header";
|
||||
import { useActiveBookingFilter } from "@/hooks/useActiveBookingFilter";
|
||||
import type { EventType } from "@/services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
|
||||
export default function Bookings() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Platform, View } from "react-native";
|
||||
import MarkNoShowScreenComponent from "../../../components/screens/MarkNoShowScreen";
|
||||
import { type Booking, CalComAPIService } from "../../../services/calcom";
|
||||
import MarkNoShowScreenComponent from "@/components/screens/MarkNoShowScreen";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
|
||||
interface Attendee {
|
||||
id?: number | string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Platform, View } from "react-native";
|
||||
import MeetingSessionDetailsScreenComponent from "../../../components/screens/MeetingSessionDetailsScreen";
|
||||
import { CalComAPIService } from "../../../services/calcom";
|
||||
import type { ConferencingSession } from "../../../services/types/bookings.types";
|
||||
import MeetingSessionDetailsScreenComponent from "@/components/screens/MeetingSessionDetailsScreen";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import type { ConferencingSession } from "@/services/types/bookings.types";
|
||||
|
||||
export default function MeetingSessionDetails() {
|
||||
const { uid } = useLocalSearchParams<{ uid: string }>();
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Platform, Text, View } from "react-native";
|
||||
import { AppPressable } from "../../../components/AppPressable";
|
||||
import type { RescheduleScreenHandle } from "../../../components/screens/RescheduleScreen";
|
||||
import RescheduleScreenComponent from "../../../components/screens/RescheduleScreen";
|
||||
import { type Booking, CalComAPIService } from "../../../services/calcom";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import type { RescheduleScreenHandle } from "@/components/screens/RescheduleScreen";
|
||||
import RescheduleScreenComponent from "@/components/screens/RescheduleScreen";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
|
||||
export default function Reschedule() {
|
||||
const { uid } = useLocalSearchParams<{ uid: string }>();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Platform, View } from "react-native";
|
||||
import ViewRecordingsScreenComponent from "../../../components/screens/ViewRecordingsScreen";
|
||||
import { CalComAPIService } from "../../../services/calcom";
|
||||
import type { BookingRecording } from "../../../services/types/bookings.types";
|
||||
import ViewRecordingsScreenComponent from "@/components/screens/ViewRecordingsScreen";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import type { BookingRecording } from "@/services/types/bookings.types";
|
||||
|
||||
export default function ViewRecordings() {
|
||||
const { uid } = useLocalSearchParams<{ uid: string }>();
|
||||
|
||||
@@ -15,26 +15,26 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { EmptyScreen } from "../../../components/EmptyScreen";
|
||||
import { EventTypeListItem } from "../../../components/event-type-list-item/EventTypeListItem";
|
||||
import { FullScreenModal } from "../../../components/FullScreenModal";
|
||||
import { Header } from "../../../components/Header";
|
||||
import { LoadingSpinner } from "../../../components/LoadingSpinner";
|
||||
import { EmptyScreen } from "@/components/EmptyScreen";
|
||||
import { EventTypeListItem } from "@/components/event-type-list-item/EventTypeListItem";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
import { Header } from "@/components/Header";
|
||||
import { LoadingSpinner } from "@/components/LoadingSpinner";
|
||||
import {
|
||||
useCreateEventType,
|
||||
useDeleteEventType,
|
||||
useDuplicateEventType,
|
||||
useEventTypes,
|
||||
useUsername,
|
||||
} from "../../../hooks";
|
||||
import { CalComAPIService, type EventType } from "../../../services/calcom";
|
||||
import { showErrorAlert } from "../../../utils/alerts";
|
||||
import { openInAppBrowser } from "../../../utils/browser";
|
||||
import { getEventDuration } from "../../../utils/getEventDuration";
|
||||
import { offlineAwareRefresh } from "../../../utils/network";
|
||||
import { normalizeMarkdown } from "../../../utils/normalizeMarkdown";
|
||||
import { shadows } from "../../../utils/shadows";
|
||||
import { slugify } from "../../../utils/slugify";
|
||||
} from "@/hooks";
|
||||
import { CalComAPIService, type EventType } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { getEventDuration } from "@/utils/getEventDuration";
|
||||
import { offlineAwareRefresh } from "@/utils/network";
|
||||
import { normalizeMarkdown } from "@/utils/normalizeMarkdown";
|
||||
import { shadows } from "@/utils/shadows";
|
||||
import { slugify } from "@/utils/slugify";
|
||||
|
||||
export default function EventTypes() {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useState } from "react";
|
||||
import { Alert, Platform, ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import { Header } from "../../components/Header";
|
||||
import { LogoutConfirmModal } from "../../components/LogoutConfirmModal";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { showErrorAlert } from "../../utils/alerts";
|
||||
import { openInAppBrowser } from "../../utils/browser";
|
||||
import { Header } from "@/components/Header";
|
||||
import { LogoutConfirmModal } from "@/components/LogoutConfirmModal";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
|
||||
interface MoreMenuItem {
|
||||
name: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Stack } from "expo-router";
|
||||
import { Platform, StatusBar, View } from "react-native";
|
||||
import LoginScreenComponent from "../components/LoginScreen";
|
||||
import { NetworkStatusBanner } from "../components/NetworkStatusBanner";
|
||||
import { AuthProvider, useAuth } from "../contexts/AuthContext";
|
||||
import { QueryProvider } from "../contexts/QueryContext";
|
||||
import LoginScreenComponent from "@/components/LoginScreen";
|
||||
import { NetworkStatusBanner } from "@/components/NetworkStatusBanner";
|
||||
import { AuthProvider, useAuth } from "@/contexts/AuthContext";
|
||||
import { QueryProvider } from "@/contexts/QueryContext";
|
||||
import "../global.css";
|
||||
|
||||
function RootLayoutContent() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Stack, useLocalSearchParams } from "expo-router";
|
||||
import { AvailabilityDetailScreen } from "../components/screens/AvailabilityDetailScreen";
|
||||
import { AvailabilityDetailScreen } from "@/components/screens/AvailabilityDetailScreen";
|
||||
|
||||
export default function AvailabilityDetail() {
|
||||
const { id } = useLocalSearchParams<{ id: string }>();
|
||||
|
||||
@@ -15,28 +15,28 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { AdvancedTab } from "../components/event-type-detail/tabs/AdvancedTab";
|
||||
import { AvailabilityTab } from "../components/event-type-detail/tabs/AvailabilityTab";
|
||||
import { BasicsTab } from "../components/event-type-detail/tabs/BasicsTab";
|
||||
import { LimitsTab } from "../components/event-type-detail/tabs/LimitsTab";
|
||||
import { RecurringTab } from "../components/event-type-detail/tabs/RecurringTab";
|
||||
import { truncateTitle } from "../components/event-type-detail/utils";
|
||||
import { buildPartialUpdatePayload } from "../components/event-type-detail/utils/buildPartialUpdatePayload";
|
||||
import { AdvancedTab } from "@/components/event-type-detail/tabs/AdvancedTab";
|
||||
import { AvailabilityTab } from "@/components/event-type-detail/tabs/AvailabilityTab";
|
||||
import { BasicsTab } from "@/components/event-type-detail/tabs/BasicsTab";
|
||||
import { LimitsTab } from "@/components/event-type-detail/tabs/LimitsTab";
|
||||
import { RecurringTab } from "@/components/event-type-detail/tabs/RecurringTab";
|
||||
import { truncateTitle } from "@/components/event-type-detail/utils";
|
||||
import { buildPartialUpdatePayload } from "@/components/event-type-detail/utils/buildPartialUpdatePayload";
|
||||
import {
|
||||
CalComAPIService,
|
||||
type ConferencingOption,
|
||||
type EventType,
|
||||
type Schedule,
|
||||
} from "../services/calcom";
|
||||
import type { LocationItem, LocationOptionGroup } from "../types/locations";
|
||||
import { showErrorAlert } from "../utils/alerts";
|
||||
import { openInAppBrowser } from "../utils/browser";
|
||||
} from "@/services/calcom";
|
||||
import type { LocationItem, LocationOptionGroup } from "@/types/locations";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import {
|
||||
buildLocationOptions,
|
||||
mapApiLocationToItem,
|
||||
mapItemToApiLocation,
|
||||
validateLocationItem,
|
||||
} from "../utils/locationHelpers";
|
||||
} from "@/utils/locationHelpers";
|
||||
|
||||
// Type definitions for extended EventType fields not in the base type
|
||||
interface EventTypeExtended {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useEffect } from "react";
|
||||
import { ActivityIndicator, Platform, Text, View } from "react-native";
|
||||
import { useAuth } from "../../contexts";
|
||||
import { useAuth } from "@/contexts";
|
||||
|
||||
export default function OAuthCallback() {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"expo-dev-client": "6.1.0-canary-20251216-3f01dbf",
|
||||
"expo-device": "8.0.11-canary-20251216-3f01dbf",
|
||||
"expo-glass-effect": "0.2.0-canary-20251216-3f01dbf",
|
||||
"expo-haptics": "^15.0.8",
|
||||
"expo-image": "^3.0.11",
|
||||
"expo-linking": "8.0.11-canary-20251216-3f01dbf",
|
||||
"expo-router": "7.0.0-canary-20251216-3f01dbf",
|
||||
"expo-secure-store": "15.0.9-canary-20251216-3f01dbf",
|
||||
@@ -1051,6 +1053,10 @@
|
||||
|
||||
"expo-glass-effect": ["expo-glass-effect@0.2.0-canary-20251216-3f01dbf", "", { "peerDependencies": { "expo": "55.0.0-canary-20251216-3f01dbf", "react": "*", "react-native": "*" } }, "sha512-n1L+Y8Gr0ZjuIu+NYZuoggBaSi7djXLbVfGxjebbe6OJczHCni06vnmDo2Zz/N2g7vqzBxZZeV/5OXo2v3YBYA=="],
|
||||
|
||||
"expo-haptics": ["expo-haptics@15.0.8", "", { "peerDependencies": { "expo": "*" } }, "sha512-lftutojy8Qs8zaDzzjwM3gKHFZ8bOOEZDCkmh2Ddpe95Ra6kt2izeOfOfKuP/QEh0MZ1j9TfqippyHdRd1ZM9g=="],
|
||||
|
||||
"expo-image": ["expo-image@3.0.11", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*", "react-native-web": "*" }, "optionalPeers": ["react-native-web"] }, "sha512-4TudfUCLgYgENv+f48omnU8tjS2S0Pd9EaON5/s1ZUBRwZ7K8acEr4NfvLPSaeXvxW24iLAiyQ7sV7BXQH3RoA=="],
|
||||
|
||||
"expo-json-utils": ["expo-json-utils@0.15.1-canary-20251216-3f01dbf", "", {}, "sha512-4pU8mIFJSXZq0jOdTEUz68SKZCKYreISzqRyFVQ5AflWyGDmZhVFgAN5jbwuzNTMhcG7+mRnWkkrOemr1aSpbg=="],
|
||||
|
||||
"expo-keep-awake": ["expo-keep-awake@15.0.9-canary-20251216-3f01dbf", "", { "peerDependencies": { "expo": "55.0.0-canary-20251216-3f01dbf", "react": "*" } }, "sha512-2BmEozgENR4fN54R39qncv+td928OJaaKz82FsKLA7hgncwM/BqPBydLUnWe3q9f+yj5iDcJlstoHC9J1D9T7w=="],
|
||||
|
||||
@@ -1,16 +1,106 @@
|
||||
import React from "react";
|
||||
import type { TouchableOpacityProps } from "react-native";
|
||||
import { TouchableOpacity } from "react-native";
|
||||
import * as Haptics from "expo-haptics";
|
||||
import React, { useCallback } from "react";
|
||||
import type { GestureResponderEvent, PressableProps, ViewStyle } from "react-native";
|
||||
import { Pressable } from "react-native";
|
||||
import Animated, {
|
||||
type SharedValue,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
type AppPressableProps = TouchableOpacityProps & {
|
||||
const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
||||
|
||||
type AppPressableProps = PressableProps & {
|
||||
className?: string;
|
||||
style?: ViewStyle;
|
||||
enableHaptics?: boolean;
|
||||
hapticStyle?: Haptics.ImpactFeedbackStyle;
|
||||
/** @deprecated Use the built-in animation instead. Kept for backward compatibility. */
|
||||
activeOpacity?: number;
|
||||
};
|
||||
|
||||
export const AppPressable = React.forwardRef<
|
||||
React.ElementRef<typeof TouchableOpacity>,
|
||||
AppPressableProps
|
||||
>(({ className, ...props }, ref) => {
|
||||
return <TouchableOpacity ref={ref} className={className} {...props} />;
|
||||
});
|
||||
function usePressAnimations(activeOpacity: number) {
|
||||
const opacity = useSharedValue(1);
|
||||
const scale = useSharedValue(1);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
transform: [{ scale: scale.value }],
|
||||
}));
|
||||
|
||||
const animatePressIn = useCallback(
|
||||
(op: SharedValue<number>, sc: SharedValue<number>) => {
|
||||
op.value = withTiming(activeOpacity, { duration: 100 });
|
||||
sc.value = withTiming(0.98, { duration: 100 });
|
||||
},
|
||||
[activeOpacity]
|
||||
);
|
||||
|
||||
const animatePressOut = useCallback((op: SharedValue<number>, sc: SharedValue<number>) => {
|
||||
op.value = withTiming(1, { duration: 100 });
|
||||
sc.value = withTiming(1, { duration: 100 });
|
||||
}, []);
|
||||
|
||||
return { animatedStyle, animatePressIn, animatePressOut, opacity, scale };
|
||||
}
|
||||
|
||||
export const AppPressable = React.forwardRef<React.ElementRef<typeof Pressable>, AppPressableProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
style,
|
||||
enableHaptics = true,
|
||||
hapticStyle = Haptics.ImpactFeedbackStyle.Light,
|
||||
activeOpacity = 0.7,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
onPress,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { animatedStyle, animatePressIn, animatePressOut, opacity, scale } =
|
||||
usePressAnimations(activeOpacity);
|
||||
|
||||
const handlePressIn = useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
animatePressIn(opacity, scale);
|
||||
onPressIn?.(event);
|
||||
},
|
||||
[animatePressIn, onPressIn, opacity, scale]
|
||||
);
|
||||
|
||||
const handlePressOut = useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
animatePressOut(opacity, scale);
|
||||
onPressOut?.(event);
|
||||
},
|
||||
[animatePressOut, onPressOut, opacity, scale]
|
||||
);
|
||||
|
||||
const handlePress = useCallback(
|
||||
(event: GestureResponderEvent) => {
|
||||
if (enableHaptics) {
|
||||
Haptics.impactAsync(hapticStyle);
|
||||
}
|
||||
onPress?.(event);
|
||||
},
|
||||
[enableHaptics, hapticStyle, onPress]
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatedPressable
|
||||
ref={ref}
|
||||
className={className}
|
||||
style={[style, animatedStyle]}
|
||||
onPressIn={handlePressIn}
|
||||
onPressOut={handlePressOut}
|
||||
onPress={handlePress}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
AppPressable.displayName = "AppPressable";
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { CalComAPIService } from "../services/calcom";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
|
||||
export function AuthTokenProvider({ children }: { children: React.ReactNode }) {
|
||||
const { accessToken, isAuthenticated, isWebSession: _isWebSession } = useAuth();
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { ScrollView, Text, TouchableOpacity, 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 type { Booking } from "@/services/calcom";
|
||||
import type { BookingActionsResult } from "@/utils/booking-actions";
|
||||
import { FullScreenModal } from "./FullScreenModal";
|
||||
|
||||
export interface BookingActionsModalProps {
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { ScrollView, Text, TouchableOpacity, View } from "react-native";
|
||||
import type { Booking } from "../services/calcom";
|
||||
import type { BookingActionsResult } from "../utils/booking-actions";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import type { BookingActionsResult } from "@/utils/booking-actions";
|
||||
import { FullScreenModal } from "./FullScreenModal";
|
||||
|
||||
export interface BookingActionsModalProps {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { useQueryContext } from "../contexts/QueryContext";
|
||||
import { useQueryContext } from "@/contexts/QueryContext";
|
||||
|
||||
/**
|
||||
* Props for the CacheStatusIndicator component
|
||||
|
||||
@@ -12,9 +12,9 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { CalComAPIService, type UserProfile } from "../services/calcom";
|
||||
import { openInAppBrowser } from "../utils/browser";
|
||||
import { getAvatarUrl } from "../utils/getAvatarUrl";
|
||||
import { CalComAPIService, type UserProfile } from "@/services/calcom";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { getAvatarUrl } from "@/utils/getAvatarUrl";
|
||||
import { CalComLogo } from "./CalComLogo";
|
||||
import { FullScreenModal } from "./FullScreenModal";
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import type { LocationItem, LocationOptionGroup } from "../types/locations";
|
||||
import type { LocationItem, LocationOptionGroup } from "@/types/locations";
|
||||
import {
|
||||
createLocationItemFromOption,
|
||||
getLocationInputLabel,
|
||||
getLocationInputPlaceholder,
|
||||
locationRequiresInput,
|
||||
} from "../utils/locationHelpers";
|
||||
} from "@/utils/locationHelpers";
|
||||
import { SvgImage } from "./SvgImage";
|
||||
|
||||
interface LocationsListProps {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Platform, Text, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { showErrorAlert } from "../utils/alerts";
|
||||
import { openInAppBrowser } from "../utils/browser";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { CalComLogo } from "./CalComLogo";
|
||||
|
||||
export function LoginScreen() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { Alert, Platform, Text, TouchableOpacity } from "react-native";
|
||||
import { useAuth } from "../contexts/AuthContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { LogoutConfirmModal } from "./LogoutConfirmModal";
|
||||
|
||||
interface LogoutButtonProps {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Image } from "expo-image";
|
||||
import type React from "react";
|
||||
import { useState } from "react";
|
||||
import { Image, View } from "react-native";
|
||||
import { View } from "react-native";
|
||||
import { SvgUri } from "react-native-svg";
|
||||
|
||||
interface SvgImageProps {
|
||||
@@ -22,13 +23,14 @@ export const SvgImage: React.FC<SvgImageProps> = ({ uri, width, height, style })
|
||||
const isSvg = pathname.toLowerCase().endsWith(".svg");
|
||||
|
||||
if (!isSvg || useFallback) {
|
||||
// Use regular Image for non-SVG or if SVG failed
|
||||
// Use expo-image for non-SVG or if SVG failed (better performance with caching)
|
||||
return (
|
||||
<View style={style}>
|
||||
<Image
|
||||
source={{ uri }}
|
||||
style={{ width, height }}
|
||||
resizeMode="contain"
|
||||
contentFit="contain"
|
||||
transition={200}
|
||||
onError={() => {
|
||||
// Image load errors are expected for invalid/missing URLs - silently handled
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
|
||||
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { Pressable, View } from "react-native";
|
||||
import type { SFSymbols7_0 } from "sf-symbols-typescript";
|
||||
import type { AvailabilityListItemProps } from "./AvailabilityListItem";
|
||||
import { AvailabilitySlots, ScheduleName, TimeZoneRow } from "./AvailabilityListItemParts";
|
||||
|
||||
export const AvailabilityListItem = ({
|
||||
item: schedule,
|
||||
@@ -45,7 +45,7 @@ export const AvailabilityListItem = ({
|
||||
|
||||
return (
|
||||
<View
|
||||
className="border-b border-[#E5E5EA] bg-white"
|
||||
className="border-b border-cal-border bg-cal-bg"
|
||||
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
||||
>
|
||||
<View className="flex-row items-center">
|
||||
@@ -55,42 +55,15 @@ export const AvailabilityListItem = ({
|
||||
accessibilityRole="button"
|
||||
>
|
||||
<View>
|
||||
<View className="mb-1 flex-row flex-wrap items-center">
|
||||
<Text className="text-base font-semibold text-[#333]">{schedule.name}</Text>
|
||||
{schedule.isDefault && (
|
||||
<View className="ml-2 rounded bg-[#666] px-2 py-0.5">
|
||||
<Text className="text-xs font-semibold text-white">Default</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{schedule.availability && schedule.availability.length > 0 ? (
|
||||
<View>
|
||||
{schedule.availability.map((slot, slotIndex) => (
|
||||
<View
|
||||
key={`${schedule.id}-${slot.days.join("-")}-${slotIndex}`}
|
||||
className={slotIndex > 0 ? "mt-2" : ""}
|
||||
>
|
||||
<Text className="text-sm text-[#666]">
|
||||
{slot.days.join(", ")} {slot.startTime} - {slot.endTime}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="text-sm text-[#666]">No availability set</Text>
|
||||
)}
|
||||
|
||||
<View className="mt-2 flex-row items-center">
|
||||
<Ionicons name="globe-outline" size={14} color="#666" />
|
||||
<Text className="ml-1.5 text-sm text-[#666]">{schedule.timeZone}</Text>
|
||||
</View>
|
||||
<ScheduleName name={schedule.name} isDefault={schedule.isDefault} />
|
||||
<AvailabilitySlots availability={schedule.availability} scheduleId={schedule.id} />
|
||||
<TimeZoneRow timeZone={schedule.timeZone} />
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
{/* Three dots menu - fixed hit target so it doesn't get squeezed off-screen */}
|
||||
<View
|
||||
className="items-center justify-center rounded-lg border border-[#E5E5EA]"
|
||||
className="items-center justify-center rounded-lg border border-cal-border"
|
||||
style={{ width: 32, height: 32 }}
|
||||
>
|
||||
<Host matchContents>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import type { Schedule } from "../../hooks";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import type { Schedule } from "@/hooks";
|
||||
import {
|
||||
AvailabilitySlots,
|
||||
ScheduleActionsButton,
|
||||
ScheduleName,
|
||||
TimeZoneRow,
|
||||
} from "./AvailabilityListItemParts";
|
||||
|
||||
export interface AvailabilityListItemProps {
|
||||
item: Schedule;
|
||||
@@ -23,57 +28,22 @@ export const AvailabilityListItem = ({
|
||||
}: AvailabilityListItemProps) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="border-b border-[#E5E5EA] bg-white active:bg-[#F8F9FA] "
|
||||
className="border-b border-cal-border bg-cal-bg active:bg-cal-bg-secondary"
|
||||
onPress={() => handleSchedulePress(schedule)}
|
||||
onLongPress={() => handleScheduleLongPress(schedule)}
|
||||
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="mr-4 flex-1">
|
||||
<View className="mb-1 flex-row flex-wrap items-center">
|
||||
<Text className="text-base font-semibold text-[#333]">{schedule.name}</Text>
|
||||
{schedule.isDefault && (
|
||||
<View className="ml-2 rounded bg-[#666] px-2 py-0.5">
|
||||
<Text className="text-xs font-semibold text-white">Default</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{schedule.availability && schedule.availability.length > 0 ? (
|
||||
<View>
|
||||
{schedule.availability.map((slot, slotIndex) => (
|
||||
<View
|
||||
key={`${schedule.id}-${slot.days.join("-")}-${slotIndex}`}
|
||||
className={slotIndex > 0 ? "mt-2" : ""}
|
||||
>
|
||||
<Text className="text-sm text-[#666]">
|
||||
{slot.days.join(", ")} {slot.startTime} - {slot.endTime}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Text className="text-sm text-[#666]">No availability set</Text>
|
||||
)}
|
||||
|
||||
<View className="mt-2 flex-row items-center">
|
||||
<Ionicons name="globe-outline" size={14} color="#666" />
|
||||
<Text className="ml-1.5 text-sm text-[#666]">{schedule.timeZone}</Text>
|
||||
</View>
|
||||
<ScheduleName name={schedule.name} isDefault={schedule.isDefault} />
|
||||
<AvailabilitySlots availability={schedule.availability} scheduleId={schedule.id} />
|
||||
<TimeZoneRow timeZone={schedule.timeZone} />
|
||||
</View>
|
||||
|
||||
{/* Three dots button - vertically centered on the right */}
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-lg border border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedSchedule(schedule);
|
||||
setShowActionsModal(true);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
<ScheduleActionsButton
|
||||
schedule={schedule}
|
||||
setSelectedSchedule={setSelectedSchedule}
|
||||
setShowActionsModal={setShowActionsModal}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import type { Schedule } from "@/hooks";
|
||||
|
||||
interface ScheduleNameProps {
|
||||
name: string;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export function ScheduleName({ name, isDefault }: ScheduleNameProps) {
|
||||
return (
|
||||
<View className="mb-1 flex-row flex-wrap items-center">
|
||||
<Text className="text-base font-semibold text-cal-text">{name}</Text>
|
||||
{isDefault && (
|
||||
<View className="ml-2 rounded bg-cal-text-secondary px-2 py-0.5">
|
||||
<Text className="text-xs font-semibold text-white">Default</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface AvailabilitySlotsProps {
|
||||
availability?: Schedule["availability"];
|
||||
scheduleId: number;
|
||||
}
|
||||
|
||||
export function AvailabilitySlots({ availability, scheduleId }: AvailabilitySlotsProps) {
|
||||
if (!availability || availability.length === 0) {
|
||||
return <Text className="text-sm text-cal-text-secondary">No availability set</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{availability.map((slot, slotIndex) => (
|
||||
<View
|
||||
key={`${scheduleId}-${slot.days.join("-")}-${slotIndex}`}
|
||||
className={slotIndex > 0 ? "mt-2" : ""}
|
||||
>
|
||||
<Text className="text-sm text-cal-text-secondary">
|
||||
{slot.days.join(", ")} {slot.startTime} - {slot.endTime}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface TimeZoneRowProps {
|
||||
timeZone: string;
|
||||
}
|
||||
|
||||
export function TimeZoneRow({ timeZone }: TimeZoneRowProps) {
|
||||
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>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface ScheduleActionsButtonProps {
|
||||
schedule: Schedule;
|
||||
setSelectedSchedule: (schedule: Schedule) => void;
|
||||
setShowActionsModal: (show: boolean) => void;
|
||||
}
|
||||
|
||||
export function ScheduleActionsButton({
|
||||
schedule,
|
||||
setSelectedSchedule,
|
||||
setShowActionsModal,
|
||||
}: ScheduleActionsButtonProps) {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-lg border border-cal-border"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
setSelectedSchedule(schedule);
|
||||
setShowActionsModal(true);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
|
||||
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import React from "react";
|
||||
import { Linking, Pressable, Text, TouchableOpacity, View } from "react-native";
|
||||
import { Pressable, View } from "react-native";
|
||||
import type { SFSymbols7_0 } from "sf-symbols-typescript";
|
||||
import { showErrorAlert } from "../../utils/alerts";
|
||||
import { getBookingActions } from "../../utils/booking-actions";
|
||||
import { formatDate, formatTime, getHostAndAttendeesDisplay } from "../../utils/bookings-utils";
|
||||
import { getMeetingInfo } from "../../utils/meetings-utils";
|
||||
import { SvgImage } from "../SvgImage";
|
||||
import { getBookingActions } from "@/utils/booking-actions";
|
||||
import {
|
||||
BadgesRow,
|
||||
BookingDescription,
|
||||
BookingTitle,
|
||||
ConfirmRejectButtons,
|
||||
HostAndAttendees,
|
||||
MeetingLink,
|
||||
TimeAndDateRow,
|
||||
} from "./BookingListItemParts";
|
||||
import type { BookingListItemProps } from "./types";
|
||||
import { useBookingListItemData } from "./useBookingListItemData";
|
||||
|
||||
export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
booking,
|
||||
@@ -31,19 +36,17 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
onReportBooking,
|
||||
onCancelBooking,
|
||||
}) => {
|
||||
const startTime = booking.start || booking.startTime || "";
|
||||
const endTime = booking.end || booking.endTime || "";
|
||||
const isUpcoming = new Date(endTime) >= new Date();
|
||||
const isPending = booking.status?.toUpperCase() === "PENDING";
|
||||
const isCancelled = booking.status?.toUpperCase() === "CANCELLED";
|
||||
const isRejected = booking.status?.toUpperCase() === "REJECTED";
|
||||
const hostAndAttendeesDisplay = getHostAndAttendeesDisplay(booking, userEmail);
|
||||
const meetingInfo = getMeetingInfo(booking.location);
|
||||
|
||||
// Check if any attendee is marked as no-show
|
||||
const hasNoShowAttendee = booking.attendees?.some(
|
||||
(att: { noShow?: boolean; absent?: boolean }) => att.noShow === true || att.absent === true
|
||||
);
|
||||
const {
|
||||
isUpcoming,
|
||||
isPending,
|
||||
isCancelled,
|
||||
isRejected,
|
||||
hostAndAttendeesDisplay,
|
||||
meetingInfo,
|
||||
hasNoShowAttendee,
|
||||
formattedDate,
|
||||
formattedTimeRange,
|
||||
} = useBookingListItemData(booking, userEmail);
|
||||
|
||||
// Use centralized action gating for consistency
|
||||
const actions = React.useMemo(() => {
|
||||
@@ -135,123 +138,32 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
.map(({ label, icon, onPress, role }) => ({ label, icon, onPress, role }));
|
||||
|
||||
return (
|
||||
<View className="border-b border-[#E5E5EA] bg-white">
|
||||
<View className="border-b border-cal-border bg-cal-bg">
|
||||
<Pressable
|
||||
onPress={() => onPress(booking)}
|
||||
onLongPress={() => onLongPress(booking)}
|
||||
style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }}
|
||||
className="active:bg-[#F8F9FA]"
|
||||
className="active:bg-cal-bg-secondary"
|
||||
>
|
||||
{/* Time and Date Row */}
|
||||
<View className="mb-2 flex-row flex-wrap items-center">
|
||||
<Text className="text-sm font-medium text-[#333]">
|
||||
{formatDate(startTime, isUpcoming)}
|
||||
</Text>
|
||||
<Text className="ml-2 text-sm text-[#666]">
|
||||
{formatTime(startTime)} - {formatTime(endTime)}
|
||||
</Text>
|
||||
</View>
|
||||
{/* Badges Row */}
|
||||
<View className="mb-3 flex-row flex-wrap items-center">
|
||||
{isPending ? (
|
||||
<View className="mb-1 mr-2 rounded bg-[#FF9500] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">Unconfirmed</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
{/* Title */}
|
||||
<Text
|
||||
className={`mb-2 text-lg font-medium leading-5 text-[#333] ${isCancelled || isRejected ? "line-through" : ""}`}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.title}
|
||||
</Text>
|
||||
{/* Description */}
|
||||
{booking.description ? (
|
||||
<Text className="mb-2 text-sm leading-5 text-[#666]" numberOfLines={1}>
|
||||
"{booking.description}"
|
||||
</Text>
|
||||
) : null}
|
||||
{/* Host and Attendees */}
|
||||
{hostAndAttendeesDisplay ? (
|
||||
<View className="mb-2 flex-row items-center">
|
||||
<Text className="text-sm text-[#333]">{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="#DC2626" />
|
||||
<Text className="ml-0.5 text-[10px] font-medium text-[#DC2626]">No-show</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
{/* Meeting Link */}
|
||||
{meetingInfo ? (
|
||||
<View className="mb-1 flex-row">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center"
|
||||
style={{ alignSelf: "flex-start" }}
|
||||
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
|
||||
onPress={async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await Linking.openURL(meetingInfo.cleanUrl);
|
||||
} catch {
|
||||
showErrorAlert("Error", "Failed to open meeting link. Please try again.");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{meetingInfo.iconUrl ? (
|
||||
<SvgImage
|
||||
uri={meetingInfo.iconUrl}
|
||||
width={16}
|
||||
height={16}
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
) : (
|
||||
<Ionicons name="videocam" size={16} color="#007AFF" style={{ marginRight: 6 }} />
|
||||
)}
|
||||
<Text className="text-sm font-medium text-[#007AFF]">{meetingInfo.label}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null}
|
||||
<TimeAndDateRow formattedDate={formattedDate} formattedTimeRange={formattedTimeRange} />
|
||||
<BadgesRow isPending={isPending} />
|
||||
<BookingTitle title={booking.title} isCancelled={isCancelled} isRejected={isRejected} />
|
||||
<BookingDescription description={booking.description} />
|
||||
<HostAndAttendees
|
||||
hostAndAttendeesDisplay={hostAndAttendeesDisplay}
|
||||
hasNoShowAttendee={hasNoShowAttendee}
|
||||
/>
|
||||
<MeetingLink meetingInfo={meetingInfo} />
|
||||
</Pressable>
|
||||
<View className="flex-row items-center justify-end gap-2">
|
||||
{isPending ? (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border border-[#E5E5EA] 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="#3C3F44" />
|
||||
<Text className="ml-1 text-sm font-medium text-[#3C3F44]">Confirm</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border border-[#E5E5EA] bg-white"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
opacity: isConfirming || isDeclining ? 0.5 : 1,
|
||||
}}
|
||||
disabled={isConfirming || isDeclining}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
onReject(booking);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={16} color="#3C3F44" />
|
||||
<Text className="ml-1 text-sm font-medium text-[#3C3F44]">Reject</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : null}
|
||||
<ConfirmRejectButtons
|
||||
booking={booking}
|
||||
isPending={isPending}
|
||||
isConfirming={isConfirming}
|
||||
isDeclining={isDeclining}
|
||||
onConfirm={onConfirm}
|
||||
onReject={onReject}
|
||||
/>
|
||||
|
||||
{/* iOS Context Menu */}
|
||||
<Host matchContents>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import type React from "react";
|
||||
import { Linking, Text, TouchableOpacity, View } from "react-native";
|
||||
import { showErrorAlert } from "../../utils/alerts";
|
||||
import { formatDate, formatTime, getHostAndAttendeesDisplay } from "../../utils/bookings-utils";
|
||||
import { getMeetingInfo } from "../../utils/meetings-utils";
|
||||
import { SvgImage } from "../SvgImage";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
BadgesRow,
|
||||
BookingDescription,
|
||||
BookingTitle,
|
||||
ConfirmRejectButtons,
|
||||
HostAndAttendees,
|
||||
MeetingLink,
|
||||
TimeAndDateRow,
|
||||
} from "./BookingListItemParts";
|
||||
import type { BookingListItemProps } from "./types";
|
||||
import { useBookingListItemData } from "./useBookingListItemData";
|
||||
|
||||
export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
booking,
|
||||
@@ -18,144 +24,49 @@ export const BookingListItem: React.FC<BookingListItemProps> = ({
|
||||
onReject,
|
||||
onActionsPress,
|
||||
}) => {
|
||||
const startTime = booking.start || booking.startTime || "";
|
||||
const endTime = booking.end || booking.endTime || "";
|
||||
const isUpcoming = new Date(endTime) >= new Date();
|
||||
const isPending = booking.status?.toUpperCase() === "PENDING";
|
||||
const isCancelled = booking.status?.toUpperCase() === "CANCELLED";
|
||||
const isRejected = booking.status?.toUpperCase() === "REJECTED";
|
||||
|
||||
const hostAndAttendeesDisplay = getHostAndAttendeesDisplay(booking, userEmail);
|
||||
const meetingInfo = getMeetingInfo(booking.location);
|
||||
|
||||
// Check if any attendee is marked as no-show
|
||||
const hasNoShowAttendee = booking.attendees?.some(
|
||||
(att: { noShow?: boolean; absent?: boolean }) => att.noShow === true || att.absent === true
|
||||
);
|
||||
const {
|
||||
isPending,
|
||||
isCancelled,
|
||||
isRejected,
|
||||
hostAndAttendeesDisplay,
|
||||
meetingInfo,
|
||||
hasNoShowAttendee,
|
||||
formattedDate,
|
||||
formattedTimeRange,
|
||||
} = useBookingListItemData(booking, userEmail);
|
||||
|
||||
return (
|
||||
<View className="border-b border-[#E5E5EA] bg-white">
|
||||
<View className="border-b border-cal-border bg-cal-bg">
|
||||
<TouchableOpacity
|
||||
className="active:bg-[#F8F9FA]"
|
||||
className="active:bg-cal-bg-secondary"
|
||||
onPress={() => onPress(booking)}
|
||||
onLongPress={() => onLongPress(booking)}
|
||||
style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }}
|
||||
>
|
||||
{/* Time and Date Row */}
|
||||
<View className="mb-2 flex-row flex-wrap items-center">
|
||||
<Text className="text-sm font-medium text-[#333]">
|
||||
{formatDate(startTime, isUpcoming)}
|
||||
</Text>
|
||||
<Text className="ml-2 text-sm text-[#666]">
|
||||
{formatTime(startTime)} - {formatTime(endTime)}
|
||||
</Text>
|
||||
</View>
|
||||
{/* Badges Row */}
|
||||
<View className="mb-3 flex-row flex-wrap items-center">
|
||||
{isPending ? (
|
||||
<View className="mb-1 mr-2 rounded bg-[#FF9500] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">Unconfirmed</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
{/* Title */}
|
||||
<Text
|
||||
className={`mb-2 text-lg font-medium leading-5 text-[#333] ${isCancelled || isRejected ? "line-through" : ""}`}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{booking.title}
|
||||
</Text>
|
||||
{/* Description */}
|
||||
{booking.description ? (
|
||||
<Text className="mb-2 text-sm leading-5 text-[#666]" numberOfLines={1}>
|
||||
"{booking.description}"
|
||||
</Text>
|
||||
) : null}
|
||||
{/* Host and Attendees */}
|
||||
{hostAndAttendeesDisplay ? (
|
||||
<View className="mb-2 flex-row items-center">
|
||||
<Text className="text-sm text-[#333]">{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="#DC2626" />
|
||||
<Text className="ml-0.5 text-[10px] font-medium text-[#DC2626]">No-show</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
{/* Meeting Link */}
|
||||
{meetingInfo ? (
|
||||
<View className="mb-1 flex-row">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center"
|
||||
style={{ alignSelf: "flex-start" }}
|
||||
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
|
||||
onPress={async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await Linking.openURL(meetingInfo.cleanUrl);
|
||||
} catch {
|
||||
showErrorAlert("Error", "Failed to open meeting link. Please try again.");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{meetingInfo.iconUrl ? (
|
||||
<SvgImage
|
||||
uri={meetingInfo.iconUrl}
|
||||
width={16}
|
||||
height={16}
|
||||
style={{ marginRight: 6 }}
|
||||
/>
|
||||
) : (
|
||||
<Ionicons name="videocam" size={16} color="#007AFF" style={{ marginRight: 6 }} />
|
||||
)}
|
||||
<Text className="text-sm font-medium text-[#007AFF]">{meetingInfo.label}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
) : null}
|
||||
<TimeAndDateRow formattedDate={formattedDate} formattedTimeRange={formattedTimeRange} />
|
||||
<BadgesRow isPending={isPending} />
|
||||
<BookingTitle title={booking.title} isCancelled={isCancelled} isRejected={isRejected} />
|
||||
<BookingDescription description={booking.description} />
|
||||
<HostAndAttendees
|
||||
hostAndAttendeesDisplay={hostAndAttendeesDisplay}
|
||||
hasNoShowAttendee={hasNoShowAttendee}
|
||||
/>
|
||||
<MeetingLink meetingInfo={meetingInfo} />
|
||||
</TouchableOpacity>
|
||||
<View
|
||||
className="flex-row items-center justify-end"
|
||||
style={{ paddingHorizontal: 16, paddingBottom: 16, gap: 8 }}
|
||||
>
|
||||
{isPending ? (
|
||||
<>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border border-[#E5E5EA] 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="#3C3F44" />
|
||||
<Text className="ml-1 text-sm font-medium text-[#3C3F44]">Confirm</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center justify-center rounded-lg border border-[#E5E5EA] bg-white"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
opacity: isConfirming || isDeclining ? 0.5 : 1,
|
||||
}}
|
||||
disabled={isConfirming || isDeclining}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
onReject(booking);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={16} color="#3C3F44" />
|
||||
<Text className="ml-1 text-sm font-medium text-[#3C3F44]">Reject</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
) : null}
|
||||
<ConfirmRejectButtons
|
||||
booking={booking}
|
||||
isPending={isPending}
|
||||
isConfirming={isConfirming}
|
||||
isDeclining={isDeclining}
|
||||
onConfirm={onConfirm}
|
||||
onReject={onReject}
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-lg border border-[#E5E5EA]"
|
||||
className="items-center justify-center rounded-lg border border-cal-border"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Linking, Text, TouchableOpacity, 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";
|
||||
|
||||
interface TimeAndDateRowProps {
|
||||
formattedDate: string;
|
||||
formattedTimeRange: string;
|
||||
}
|
||||
|
||||
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>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface BadgesRowProps {
|
||||
isPending: boolean;
|
||||
}
|
||||
|
||||
export function BadgesRow({ isPending }: BadgesRowProps) {
|
||||
return (
|
||||
<View className="mb-3 flex-row flex-wrap items-center">
|
||||
{isPending ? (
|
||||
<View className="mb-1 mr-2 rounded bg-cal-accent-warning px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">Unconfirmed</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface BookingTitleProps {
|
||||
title: string;
|
||||
isCancelled: boolean;
|
||||
isRejected: boolean;
|
||||
}
|
||||
|
||||
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" : ""}`}
|
||||
numberOfLines={2}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
interface BookingDescriptionProps {
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export function BookingDescription({ description }: BookingDescriptionProps) {
|
||||
if (!description) return null;
|
||||
return (
|
||||
<Text className="mb-2 text-sm leading-5 text-cal-text-secondary" numberOfLines={1}>
|
||||
"{description}"
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
interface HostAndAttendeesProps {
|
||||
hostAndAttendeesDisplay: string | null;
|
||||
hasNoShowAttendee: boolean;
|
||||
}
|
||||
|
||||
export function HostAndAttendees({
|
||||
hostAndAttendeesDisplay,
|
||||
hasNoShowAttendee,
|
||||
}: HostAndAttendeesProps) {
|
||||
if (!hostAndAttendeesDisplay) return null;
|
||||
return (
|
||||
<View className="mb-2 flex-row items-center">
|
||||
<Text className="text-sm text-cal-text">{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="#DC2626" />
|
||||
<Text className="ml-0.5 text-[10px] font-medium text-cal-accent-destructive">
|
||||
No-show
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface MeetingLinkProps {
|
||||
meetingInfo: BookingListItemData["meetingInfo"];
|
||||
}
|
||||
|
||||
export function MeetingLink({ meetingInfo }: MeetingLinkProps) {
|
||||
if (!meetingInfo) return null;
|
||||
return (
|
||||
<View className="mb-1 flex-row">
|
||||
<TouchableOpacity
|
||||
className="flex-row items-center"
|
||||
style={{ alignSelf: "flex-start" }}
|
||||
hitSlop={{ top: 4, bottom: 4, left: 4, right: 4 }}
|
||||
onPress={async (e) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
await Linking.openURL(meetingInfo.cleanUrl);
|
||||
} catch {
|
||||
showErrorAlert("Error", "Failed to open meeting link. Please try again.");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{meetingInfo.iconUrl ? (
|
||||
<SvgImage uri={meetingInfo.iconUrl} width={16} height={16} style={{ marginRight: 6 }} />
|
||||
) : (
|
||||
<Ionicons name="videocam" size={16} color="#007AFF" style={{ marginRight: 6 }} />
|
||||
)}
|
||||
<Text className="text-sm font-medium text-cal-accent">{meetingInfo.label}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConfirmRejectButtonsProps {
|
||||
booking: Booking;
|
||||
isPending: boolean;
|
||||
isConfirming: boolean;
|
||||
isDeclining: boolean;
|
||||
onConfirm: (booking: Booking) => void;
|
||||
onReject: (booking: Booking) => void;
|
||||
}
|
||||
|
||||
export function ConfirmRejectButtons({
|
||||
booking,
|
||||
isPending,
|
||||
isConfirming,
|
||||
isDeclining,
|
||||
onConfirm,
|
||||
onReject,
|
||||
}: ConfirmRejectButtonsProps) {
|
||||
if (!isPending) 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"
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
opacity: isConfirming || isDeclining ? 0.5 : 1,
|
||||
}}
|
||||
disabled={isConfirming || isDeclining}
|
||||
onPress={(e) => {
|
||||
e.stopPropagation();
|
||||
onReject(booking);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="close" size={16} color="#3C3F44" />
|
||||
<Text className="ml-1 text-sm font-medium text-cal-text-emphasis">Reject</Text>
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Booking } from "../../services/calcom";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
|
||||
export interface BookingListItemProps {
|
||||
booking: Booking;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { formatDate, formatTime, getHostAndAttendeesDisplay } from "@/utils/bookings-utils";
|
||||
import { getMeetingInfo } from "@/utils/meetings-utils";
|
||||
|
||||
export interface BookingListItemData {
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
isUpcoming: boolean;
|
||||
isPending: boolean;
|
||||
isCancelled: boolean;
|
||||
isRejected: boolean;
|
||||
hostAndAttendeesDisplay: string | null;
|
||||
meetingInfo: ReturnType<typeof getMeetingInfo>;
|
||||
hasNoShowAttendee: boolean;
|
||||
formattedDate: string;
|
||||
formattedTimeRange: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and computes derived data from a booking object.
|
||||
* This hook centralizes the logic shared between iOS and non-iOS BookingListItem components.
|
||||
*/
|
||||
export function useBookingListItemData(booking: Booking, userEmail?: string): BookingListItemData {
|
||||
const startTime = booking.start || booking.startTime || "";
|
||||
const endTime = booking.end || booking.endTime || "";
|
||||
const isUpcoming = new Date(endTime) >= new Date();
|
||||
const isPending = booking.status?.toUpperCase() === "PENDING";
|
||||
const isCancelled = booking.status?.toUpperCase() === "CANCELLED";
|
||||
const isRejected = booking.status?.toUpperCase() === "REJECTED";
|
||||
|
||||
const hostAndAttendeesDisplay = getHostAndAttendeesDisplay(booking, userEmail);
|
||||
const meetingInfo = getMeetingInfo(booking.location);
|
||||
|
||||
const hasNoShowAttendee =
|
||||
booking.attendees?.some(
|
||||
(att: { noShow?: boolean; absent?: boolean }) => att.noShow === true || att.absent === true
|
||||
) ?? false;
|
||||
|
||||
const formattedDate = formatDate(startTime, isUpcoming);
|
||||
const formattedTimeRange = `${formatTime(startTime)} - ${formatTime(endTime)}`;
|
||||
|
||||
return {
|
||||
startTime,
|
||||
endTime,
|
||||
isUpcoming,
|
||||
isPending,
|
||||
isCancelled,
|
||||
isRejected,
|
||||
hostAndAttendeesDisplay,
|
||||
meetingInfo,
|
||||
hasNoShowAttendee,
|
||||
formattedDate,
|
||||
formattedTimeRange,
|
||||
};
|
||||
}
|
||||
@@ -2,7 +2,11 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import React, { Activity, useMemo, useState } from "react";
|
||||
import { Alert, FlatList, RefreshControl, ScrollView, Text, View } from "react-native";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { BookingListItem } from "@/components/booking-list-item/BookingListItem";
|
||||
import { BookingModals } from "@/components/booking-modals/BookingModals";
|
||||
import { EmptyScreen } from "@/components/EmptyScreen";
|
||||
import { LoadingSpinner } from "@/components/LoadingSpinner";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import {
|
||||
type BookingFilter,
|
||||
useBookingActions,
|
||||
@@ -11,20 +15,16 @@ import {
|
||||
useConfirmBooking,
|
||||
useDeclineBooking,
|
||||
useRescheduleBooking,
|
||||
} from "../../hooks";
|
||||
import type { Booking, EventType } from "../../services/calcom";
|
||||
import type { ListItem } from "../../utils/bookings-utils";
|
||||
} from "@/hooks";
|
||||
import type { Booking, EventType } from "@/services/calcom";
|
||||
import type { ListItem } from "@/utils/bookings-utils";
|
||||
import {
|
||||
filterByEventType,
|
||||
getEmptyStateContent,
|
||||
groupBookingsByMonth,
|
||||
searchBookings,
|
||||
} from "../../utils/bookings-utils";
|
||||
import { offlineAwareRefresh } from "../../utils/network";
|
||||
import { BookingListItem } from "../booking-list-item/BookingListItem";
|
||||
import { BookingModals } from "../booking-modals/BookingModals";
|
||||
import { EmptyScreen } from "../EmptyScreen";
|
||||
import { LoadingSpinner } from "../LoadingSpinner";
|
||||
} from "@/utils/bookings-utils";
|
||||
import { offlineAwareRefresh } from "@/utils/network";
|
||||
|
||||
interface BookingListScreenProps {
|
||||
// Platform-specific header rendering
|
||||
|
||||
@@ -10,11 +10,10 @@ import {
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
import type { Booking, EventType } from "../../services/calcom";
|
||||
import { type BookingActionsResult, getBookingActions } from "../../utils/booking-actions";
|
||||
import { BookingActionsModal } from "../BookingActionsModal";
|
||||
import { FullScreenModal } from "../FullScreenModal";
|
||||
import { BookingActionsModal } from "@/components/BookingActionsModal";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
import type { Booking, EventType } from "@/services/calcom";
|
||||
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
|
||||
|
||||
// Empty actions result for when no booking is selected
|
||||
const EMPTY_ACTIONS: BookingActionsResult = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Alert, Switch, Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
|
||||
import { openInAppBrowser } from "../../../utils/browser";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
|
||||
interface ConfigureOnWebCardProps {
|
||||
title: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import type { Schedule } from "../../../services/calcom";
|
||||
import type { Schedule } from "@/services/calcom";
|
||||
|
||||
interface DaySchedule {
|
||||
day: string;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Switch, Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
|
||||
import { LocationsList } from "../../../components/LocationsList";
|
||||
import type { LocationItem, LocationOptionGroup } from "../../../types/locations";
|
||||
import { slugify } from "../../../utils/slugify";
|
||||
import { LocationsList } from "@/components/LocationsList";
|
||||
import type { LocationItem, LocationOptionGroup } from "@/types/locations";
|
||||
import { slugify } from "@/utils/slugify";
|
||||
|
||||
interface BasicsTabProps {
|
||||
// Title, description, URL
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Switch, Text, TextInput, TouchableOpacity, View } from "react-native";
|
||||
import { openInAppBrowser } from "../../../utils/browser";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
|
||||
interface RecurringTabProps {
|
||||
recurringEnabled: boolean;
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
*/
|
||||
|
||||
// Re-export formatting utilities from centralized location
|
||||
export { formatAppIdToDisplayName, formatDuration, truncateTitle } from "../../utils/formatters";
|
||||
export { formatAppIdToDisplayName, formatDuration, truncateTitle } from "@/utils/formatters";
|
||||
// Re-export location utilities from centralized location
|
||||
export { displayNameToLocationValue } from "../../utils/locationHelpers";
|
||||
export { displayNameToLocationValue } from "@/utils/locationHelpers";
|
||||
// Re-export partial update utilities
|
||||
export { buildPartialUpdatePayload, hasChanges } from "./utils/buildPartialUpdatePayload";
|
||||
@@ -1,16 +1,12 @@
|
||||
import type {
|
||||
BookingLimitsCount,
|
||||
BookingLimitsDuration,
|
||||
EventType,
|
||||
} from "../../../services/calcom";
|
||||
import type { LocationItem } from "../../../types/locations";
|
||||
import type { BookingLimitsCount, BookingLimitsDuration, EventType } from "@/services/calcom";
|
||||
import type { LocationItem } from "@/types/locations";
|
||||
import {
|
||||
parseBufferTime,
|
||||
parseFrequencyUnit,
|
||||
parseMinimumNotice,
|
||||
parseSlotInterval,
|
||||
} from "../../../utils/eventTypeParsers";
|
||||
import { mapItemToApiLocation } from "../../../utils/locationHelpers";
|
||||
} from "@/utils/eventTypeParsers";
|
||||
import { mapItemToApiLocation } from "@/utils/locationHelpers";
|
||||
|
||||
interface LocationInput {
|
||||
type: string;
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Button, ContextMenu, Host, HStack, Image } from "@expo/ui/swift-ui";
|
||||
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import type React from "react";
|
||||
import { Pressable, Text, View } from "react-native";
|
||||
import { formatDuration } from "../../utils/formatters";
|
||||
import { getEventDuration } from "../../utils/getEventDuration";
|
||||
import { normalizeMarkdown } from "../../utils/normalizeMarkdown";
|
||||
import { Pressable, View } from "react-native";
|
||||
import {
|
||||
DurationBadge,
|
||||
EventTypeDescription,
|
||||
EventTypeTitle,
|
||||
PriceAndConfirmationBadges,
|
||||
} from "./EventTypeListItemParts";
|
||||
import type { EventTypeListItemProps } from "./types";
|
||||
import { useEventTypeListItemData } from "./useEventTypeListItemData";
|
||||
|
||||
export const EventTypeListItem = ({
|
||||
item,
|
||||
@@ -22,7 +25,8 @@ export const EventTypeListItem = ({
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
}: EventTypeListItemProps) => {
|
||||
const duration = getEventDuration(item);
|
||||
const { formattedDuration, normalizedDescription, hasPrice, formattedPrice } =
|
||||
useEventTypeListItemData(item);
|
||||
const isLast = index === filteredEventTypes.length - 1;
|
||||
|
||||
type ButtonSystemImage = React.ComponentProps<typeof Button>["systemImage"];
|
||||
@@ -67,7 +71,9 @@ export const EventTypeListItem = ({
|
||||
];
|
||||
|
||||
return (
|
||||
<View className={`bg-white active:bg-[#F8F9FA] ${!isLast ? "border-b border-[#E5E5EA]" : ""}`}>
|
||||
<View
|
||||
className={`bg-cal-bg active:bg-cal-bg-secondary ${!isLast ? "border-b border-cal-border" : ""}`}
|
||||
>
|
||||
<View className="flex-shrink-1 flex-row items-center justify-between">
|
||||
<Pressable
|
||||
onPress={() => handleEventTypePress(item)}
|
||||
@@ -75,35 +81,14 @@ export const EventTypeListItem = ({
|
||||
className="flex-grow"
|
||||
>
|
||||
<View className="mr-4 flex-1">
|
||||
<View className="mb-1 flex-row items-center">
|
||||
<Text className="flex-1 text-base font-semibold text-[#333]">{item.title} </Text>
|
||||
</View>
|
||||
{item.description ? (
|
||||
<Text className="mb-2 mt-0.5 text-sm leading-5 text-[#666]" numberOfLines={2}>
|
||||
{normalizeMarkdown(item.description)}
|
||||
</Text>
|
||||
) : null}
|
||||
<View className="mt-2 flex-row items-center self-start rounded-lg border border-[#E5E5EA] bg-[#E5E5EA] px-2 py-1">
|
||||
<Ionicons name="time-outline" size={14} color="#000" />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-black">
|
||||
{formatDuration(duration)}
|
||||
</Text>
|
||||
</View>
|
||||
{(item.price != null && item.price > 0) || item.requiresConfirmation ? (
|
||||
<View className="mt-2 flex-row items-center gap-3">
|
||||
{item.price != null && item.price > 0 ? (
|
||||
<Text className="text-sm font-medium text-[#34C759]">
|
||||
{item.currency || "$"}
|
||||
{item.price}
|
||||
</Text>
|
||||
) : null}
|
||||
{item.requiresConfirmation ? (
|
||||
<View className="rounded bg-[#FF9500] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">Requires Confirmation</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
<EventTypeTitle title={item.title} />
|
||||
<EventTypeDescription normalizedDescription={normalizedDescription} />
|
||||
<DurationBadge formattedDuration={formattedDuration} />
|
||||
<PriceAndConfirmationBadges
|
||||
hasPrice={hasPrice}
|
||||
formattedPrice={formattedPrice}
|
||||
requiresConfirmation={item.requiresConfirmation}
|
||||
/>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Text, TouchableOpacity, View } from "react-native";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
import { Tooltip } from "../../components/Tooltip";
|
||||
import { formatDuration } from "../../utils/formatters";
|
||||
import { getEventDuration } from "../../utils/getEventDuration";
|
||||
import { normalizeMarkdown } from "../../utils/normalizeMarkdown";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
DurationBadge,
|
||||
EventTypeActions,
|
||||
EventTypeDescription,
|
||||
EventTypeTitle,
|
||||
PriceAndConfirmationBadges,
|
||||
} from "./EventTypeListItemParts";
|
||||
import type { EventTypeListItemProps } from "./types";
|
||||
import { useEventTypeListItemData } from "./useEventTypeListItemData";
|
||||
|
||||
export const EventTypeListItem = ({
|
||||
item,
|
||||
@@ -17,93 +19,35 @@ export const EventTypeListItem = ({
|
||||
handleCopyLink,
|
||||
handlePreview,
|
||||
}: EventTypeListItemProps) => {
|
||||
const duration = getEventDuration(item);
|
||||
const { formattedDuration, normalizedDescription, hasPrice, formattedPrice } =
|
||||
useEventTypeListItemData(item);
|
||||
const isLast = index === filteredEventTypes.length - 1;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className={`bg-white active:bg-[#F8F9FA] ${!isLast ? "border-b border-[#E5E5EA]" : ""}`}
|
||||
className={`bg-cal-bg active:bg-cal-bg-secondary ${!isLast ? "border-b border-cal-border" : ""}`}
|
||||
onPress={() => handleEventTypePress(item)}
|
||||
onLongPress={() => handleEventTypeLongPress(item)}
|
||||
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="mr-4 flex-1">
|
||||
<View className="mb-1 flex-row items-center">
|
||||
<Text className="flex-1 text-base font-semibold text-[#333]">{item.title}</Text>
|
||||
</View>
|
||||
{item.description ? (
|
||||
<Text className="mb-2 mt-0.5 text-sm leading-5 text-[#666]" numberOfLines={2}>
|
||||
{normalizeMarkdown(item.description)}
|
||||
</Text>
|
||||
) : null}
|
||||
<View className="mt-2 flex-row items-center self-start rounded-lg border border-[#E5E5EA] bg-[#E5E5EA] px-2 py-1">
|
||||
<Ionicons name="time-outline" size={14} color="#000" />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-black">
|
||||
{formatDuration(duration)}
|
||||
</Text>
|
||||
</View>
|
||||
{(item.price != null && item.price > 0) || item.requiresConfirmation ? (
|
||||
<View className="mt-2 flex-row items-center gap-3">
|
||||
{item.price != null && item.price > 0 ? (
|
||||
<Text className="text-sm font-medium text-[#34C759]">
|
||||
{item.currency || "$"}
|
||||
{item.price}
|
||||
</Text>
|
||||
) : null}
|
||||
{item.requiresConfirmation ? (
|
||||
<View className="rounded bg-[#FF9500] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">Requires Confirmation</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<View className="flex-row">
|
||||
<Tooltip text="Preview">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-l-lg border border-r-0 border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handlePreview(item)}
|
||||
>
|
||||
<Ionicons name="open-outline" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<Tooltip text={copiedEventTypeId === item.id ? "Copied!" : "Copy link"}>
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center border border-r-0 border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleCopyLink(item)}
|
||||
>
|
||||
{copiedEventTypeId === item.id ? (
|
||||
<Ionicons name="checkmark" size={18} color="#10B981" />
|
||||
) : (
|
||||
<Svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#3C3F44"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<Path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<Path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</Svg>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<Tooltip text="More">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-r-lg border border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleEventTypeLongPress(item)}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<EventTypeTitle title={item.title} />
|
||||
<EventTypeDescription normalizedDescription={normalizedDescription} />
|
||||
<DurationBadge formattedDuration={formattedDuration} />
|
||||
<PriceAndConfirmationBadges
|
||||
hasPrice={hasPrice}
|
||||
formattedPrice={formattedPrice}
|
||||
requiresConfirmation={item.requiresConfirmation}
|
||||
/>
|
||||
</View>
|
||||
<EventTypeActions
|
||||
item={item}
|
||||
copiedEventTypeId={copiedEventTypeId}
|
||||
handleCopyLink={handleCopyLink}
|
||||
handlePreview={handlePreview}
|
||||
handleEventTypeLongPress={handleEventTypeLongPress}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { Text, TouchableOpacity, 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";
|
||||
|
||||
interface EventTypeTitleProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function EventTypeTitle({ title }: EventTypeTitleProps) {
|
||||
return (
|
||||
<View className="mb-1 flex-row items-center">
|
||||
<Text className="flex-1 text-base font-semibold text-cal-text">{title}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface EventTypeDescriptionProps {
|
||||
normalizedDescription: string | null;
|
||||
}
|
||||
|
||||
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}>
|
||||
{normalizedDescription}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
interface DurationBadgeProps {
|
||||
formattedDuration: string;
|
||||
}
|
||||
|
||||
export function DurationBadge({ formattedDuration }: DurationBadgeProps) {
|
||||
return (
|
||||
<View className="mt-2 flex-row items-center self-start rounded-lg border border-cal-border bg-cal-border px-2 py-1">
|
||||
<Ionicons name="time-outline" size={14} color="#000000" />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-cal-brand-black">{formattedDuration}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface PriceAndConfirmationBadgesProps {
|
||||
hasPrice: boolean;
|
||||
formattedPrice: string | null;
|
||||
requiresConfirmation?: boolean;
|
||||
}
|
||||
|
||||
export function PriceAndConfirmationBadges({
|
||||
hasPrice,
|
||||
formattedPrice,
|
||||
requiresConfirmation,
|
||||
}: PriceAndConfirmationBadgesProps) {
|
||||
if (!hasPrice && !requiresConfirmation) return null;
|
||||
return (
|
||||
<View className="mt-2 flex-row items-center gap-3">
|
||||
{hasPrice && formattedPrice ? (
|
||||
<Text className="text-sm font-medium text-cal-accent-success">{formattedPrice}</Text>
|
||||
) : null}
|
||||
{requiresConfirmation ? (
|
||||
<View className="rounded bg-cal-accent-warning px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">Requires Confirmation</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface EventTypeActionsProps {
|
||||
item: EventType;
|
||||
copiedEventTypeId: number | null;
|
||||
handleCopyLink: (item: EventType) => void;
|
||||
handlePreview: (item: EventType) => void;
|
||||
handleEventTypeLongPress: (item: EventType) => void;
|
||||
}
|
||||
|
||||
export function EventTypeActions({
|
||||
item,
|
||||
copiedEventTypeId,
|
||||
handleCopyLink,
|
||||
handlePreview,
|
||||
handleEventTypeLongPress,
|
||||
}: EventTypeActionsProps) {
|
||||
return (
|
||||
<View className="flex-row">
|
||||
<Tooltip text="Preview">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-l-lg border border-r-0 border-cal-border"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handlePreview(item)}
|
||||
>
|
||||
<Ionicons name="open-outline" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<Tooltip text={copiedEventTypeId === item.id ? "Copied!" : "Copy link"}>
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center border border-r-0 border-cal-border"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleCopyLink(item)}
|
||||
>
|
||||
{copiedEventTypeId === item.id ? (
|
||||
<Ionicons name="checkmark" size={18} color="#34C759" />
|
||||
) : (
|
||||
<Svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#3C3F44"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<Path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<Path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</Svg>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<Tooltip text="More">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-r-lg border border-cal-border"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleEventTypeLongPress(item)}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EventType } from "../../services/types/event-types.types";
|
||||
import type { EventType } from "@/services/types/event-types.types";
|
||||
|
||||
export interface EventTypeListItemProps {
|
||||
item: EventType;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { EventType } from "@/services/types/event-types.types";
|
||||
import { formatDuration } from "@/utils/formatters";
|
||||
import { getEventDuration } from "@/utils/getEventDuration";
|
||||
import { normalizeMarkdown } from "@/utils/normalizeMarkdown";
|
||||
|
||||
export interface EventTypeListItemData {
|
||||
duration: number;
|
||||
formattedDuration: string;
|
||||
normalizedDescription: string | null;
|
||||
hasPrice: boolean;
|
||||
formattedPrice: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and computes derived data from an event type object.
|
||||
* This hook centralizes the logic shared between iOS and non-iOS EventTypeListItem components.
|
||||
*/
|
||||
export function useEventTypeListItemData(item: EventType): EventTypeListItemData {
|
||||
const duration = getEventDuration(item);
|
||||
const formattedDuration = formatDuration(duration);
|
||||
const normalizedDescription = item.description ? normalizeMarkdown(item.description) : null;
|
||||
const hasPrice = item.price != null && item.price > 0;
|
||||
const formattedPrice = hasPrice ? `${item.currency || "$"}${item.price}` : null;
|
||||
|
||||
return {
|
||||
duration,
|
||||
formattedDuration,
|
||||
normalizedDescription,
|
||||
hasPrice,
|
||||
formattedPrice,
|
||||
};
|
||||
}
|
||||
@@ -18,8 +18,8 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { Booking } from "../../services/calcom";
|
||||
import { CalComAPIService } from "../../services/calcom";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
|
||||
export interface AddGuestsScreenProps {
|
||||
booking: Booking | null;
|
||||
|
||||
@@ -4,11 +4,11 @@ import { useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, Alert, ScrollView, Switch, Text, TextInput, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { CalComAPIService, type Schedule } from "../../services/calcom";
|
||||
import type { ScheduleAvailability } from "../../services/types";
|
||||
import { showErrorAlert } from "../../utils/alerts";
|
||||
import { AppPressable } from "../AppPressable";
|
||||
import { FullScreenModal } from "../FullScreenModal";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
import type { ScheduleAvailability } from "@/services/types";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
const DAY_ABBREVIATIONS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
|
||||
@@ -12,23 +12,23 @@ import {
|
||||
TextInput,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { AppPressable } from "../../components/AppPressable";
|
||||
import { AvailabilityListItem } from "../../components/availability-list-item/AvailabilityListItem";
|
||||
import { EmptyScreen } from "../../components/EmptyScreen";
|
||||
import { FullScreenModal } from "../../components/FullScreenModal";
|
||||
import { Header } from "../../components/Header";
|
||||
import { LoadingSpinner } from "../../components/LoadingSpinner";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { AvailabilityListItem } from "@/components/availability-list-item/AvailabilityListItem";
|
||||
import { EmptyScreen } from "@/components/EmptyScreen";
|
||||
import { FullScreenModal } from "@/components/FullScreenModal";
|
||||
import { Header } from "@/components/Header";
|
||||
import { LoadingSpinner } from "@/components/LoadingSpinner";
|
||||
import {
|
||||
useCreateSchedule,
|
||||
useDeleteSchedule,
|
||||
useDuplicateSchedule,
|
||||
useSchedules,
|
||||
useSetScheduleAsDefault,
|
||||
} from "../../hooks";
|
||||
import { CalComAPIService, type Schedule } from "../../services/calcom";
|
||||
import { showErrorAlert } from "../../utils/alerts";
|
||||
import { offlineAwareRefresh } from "../../utils/network";
|
||||
import { shadows } from "../../utils/shadows";
|
||||
} from "@/hooks";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { offlineAwareRefresh } from "@/utils/network";
|
||||
import { shadows } from "@/utils/shadows";
|
||||
|
||||
export interface AvailabilityListScreenProps {
|
||||
searchQuery: string;
|
||||
|
||||
@@ -2,17 +2,17 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { ActivityIndicator, Alert, Platform, ScrollView, Text, View } from "react-native";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { type Booking, CalComAPIService } from "../../services/calcom";
|
||||
import { showErrorAlert } from "../../utils/alerts";
|
||||
import { type BookingActionsResult, getBookingActions } from "../../utils/booking-actions";
|
||||
import { openInAppBrowser } from "../../utils/browser";
|
||||
import { defaultLocations, getDefaultLocationIconUrl } from "../../utils/defaultLocations";
|
||||
import { formatAppIdToDisplayName } from "../../utils/formatters";
|
||||
import { getAppIconUrl } from "../../utils/getAppIconUrl";
|
||||
import { AppPressable } from "../AppPressable";
|
||||
import { BookingActionsModal } from "../BookingActionsModal";
|
||||
import { SvgImage } from "../SvgImage";
|
||||
import { AppPressable } from "@/components/AppPressable";
|
||||
import { BookingActionsModal } from "@/components/BookingActionsModal";
|
||||
import { SvgImage } from "@/components/SvgImage";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
import { type BookingActionsResult, getBookingActions } from "@/utils/booking-actions";
|
||||
import { openInAppBrowser } from "@/utils/browser";
|
||||
import { defaultLocations, getDefaultLocationIconUrl } from "@/utils/defaultLocations";
|
||||
import { formatAppIdToDisplayName } from "@/utils/formatters";
|
||||
import { getAppIconUrl } from "@/utils/getAppIconUrl";
|
||||
|
||||
// Empty actions result for when no booking is loaded
|
||||
const EMPTY_ACTIONS: BookingActionsResult = {
|
||||
|
||||
@@ -12,8 +12,8 @@ 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 { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { Booking } from "../../services/calcom";
|
||||
import { CalComAPIService } from "../../services/calcom";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
|
||||
// Location types configuration
|
||||
export const LOCATION_TYPES = {
|
||||
|
||||
@@ -29,8 +29,8 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { Booking } from "../../services/calcom";
|
||||
import { CalComAPIService } from "../../services/calcom";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
|
||||
export const LOCATION_TYPES = {
|
||||
link: {
|
||||
|
||||
@@ -9,8 +9,8 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { useState } from "react";
|
||||
import { ActivityIndicator, Alert, FlatList, Text, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { Booking } from "../../services/calcom";
|
||||
import { CalComAPIService } from "../../services/calcom";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
|
||||
interface Attendee {
|
||||
id?: number | string;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { FlatList, Text, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { ConferencingSession } from "../../services/types/bookings.types";
|
||||
import type { ConferencingSession } from "@/services/types/bookings.types";
|
||||
|
||||
export interface MeetingSessionDetailsScreenProps {
|
||||
sessions: ConferencingSession[];
|
||||
|
||||
@@ -19,8 +19,8 @@ import {
|
||||
View,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { Booking } from "../../services/calcom";
|
||||
import { CalComAPIService } from "../../services/calcom";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
|
||||
// Note: @expo/ui DateTimePicker components are not yet stable
|
||||
// Using a simple inline picker approach instead
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import * as WebBrowser from "expo-web-browser";
|
||||
import { FlatList, Text, TouchableOpacity, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { BookingRecording } from "../../services/types/bookings.types";
|
||||
import type { BookingRecording } from "@/services/types/bookings.types";
|
||||
|
||||
export interface ViewRecordingsScreenProps {
|
||||
recordings: BookingRecording[];
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { createContext, type ReactNode, useCallback, useContext, useEffect, useState } from "react";
|
||||
import { CalComAPIService } from "../services/calcom";
|
||||
import { CalComAPIService } from "@/services/calcom";
|
||||
import {
|
||||
type CalComOAuthService,
|
||||
createCalComOAuthService,
|
||||
type OAuthTokens,
|
||||
} from "../services/oauthService";
|
||||
import type { UserProfile } from "../services/types/users.types";
|
||||
import { WebAuthService } from "../services/webAuth";
|
||||
import { secureStorage } from "../utils/storage";
|
||||
} from "@/services/oauthService";
|
||||
import type { UserProfile } from "@/services/types/users.types";
|
||||
import { WebAuthService } from "@/services/webAuth";
|
||||
import { secureStorage } from "@/utils/storage";
|
||||
|
||||
/**
|
||||
* Simplified user info stored in auth context
|
||||
|
||||
@@ -12,8 +12,8 @@ import { onlineManager, QueryClient } from "@tanstack/react-query";
|
||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||
import React, { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Platform } from "react-native";
|
||||
import { CACHE_CONFIG } from "../config/cache.config";
|
||||
import { clearQueryCache, createQueryPersister } from "../utils/queryPersister";
|
||||
import { CACHE_CONFIG } from "@/config/cache.config";
|
||||
import { clearQueryCache, createQueryPersister } from "@/utils/queryPersister";
|
||||
|
||||
/**
|
||||
* Create and configure the QueryClient instance
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
*/
|
||||
|
||||
// Re-export query keys for advanced use cases
|
||||
export { queryKeys } from "../config/cache.config";
|
||||
export { queryKeys } from "@/config/cache.config";
|
||||
// Re-export query context utilities
|
||||
export { useQueryContext } from "../contexts/QueryContext";
|
||||
export { useQueryContext } from "@/contexts/QueryContext";
|
||||
// UI State Management hooks
|
||||
export {
|
||||
type BookingFilter,
|
||||
|
||||
@@ -8,14 +8,14 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Alert, Linking } from "react-native";
|
||||
import { queryKeys } from "../config/cache.config";
|
||||
import { type Booking, CalComAPIService } from "../services/calcom";
|
||||
import { queryKeys } from "@/config/cache.config";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
import type {
|
||||
AddGuestInput,
|
||||
BookingRecording,
|
||||
ConferencingSession,
|
||||
} from "../services/types/bookings.types";
|
||||
import { showErrorAlert } from "../utils/alerts";
|
||||
} from "@/services/types/bookings.types";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
interface UseBookingActionModalsReturn {
|
||||
// Selected booking for actions
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { useRouter } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import type { Booking } from "../services/calcom";
|
||||
import { showErrorAlert } from "../utils/alerts";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import { showErrorAlert } from "@/utils/alerts";
|
||||
|
||||
interface UseBookingActionsParams {
|
||||
router: ReturnType<typeof useRouter>;
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
*/
|
||||
|
||||
import { useMemo } from "react";
|
||||
import type { Booking } from "../services/calcom";
|
||||
import type { EventType } from "../services/types/event-types.types";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
import type { EventType } from "@/services/types/event-types.types";
|
||||
import {
|
||||
type BookingActionsResult,
|
||||
getBookingActions,
|
||||
type NormalizedBooking,
|
||||
normalizeBooking,
|
||||
} from "../utils/booking-actions";
|
||||
} from "@/utils/booking-actions";
|
||||
|
||||
interface UseBookingActionsGatingParams {
|
||||
booking: Booking | null;
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
|
||||
import { type Booking, CalComAPIService } from "../services/calcom";
|
||||
import { CACHE_CONFIG, queryKeys } from "@/config/cache.config";
|
||||
import { type Booking, CalComAPIService } from "@/services/calcom";
|
||||
|
||||
/**
|
||||
* Filter options for fetching bookings
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
|
||||
import { CalComAPIService, type CreateEventTypeInput, type EventType } from "../services/calcom";
|
||||
import { CACHE_CONFIG, queryKeys } from "@/config/cache.config";
|
||||
import { CalComAPIService, type CreateEventTypeInput, type EventType } from "@/services/calcom";
|
||||
|
||||
/**
|
||||
* Hook to fetch all event types
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
|
||||
import { CalComAPIService, type Schedule } from "../services/calcom";
|
||||
import { CACHE_CONFIG, queryKeys } from "@/config/cache.config";
|
||||
import { CalComAPIService, type Schedule } from "@/services/calcom";
|
||||
|
||||
/**
|
||||
* Sort schedules: default first, then alphabetically by name
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
*/
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { CACHE_CONFIG, queryKeys } from "../config/cache.config";
|
||||
import { CalComAPIService, type UserProfile } from "../services/calcom";
|
||||
import { CACHE_CONFIG, queryKeys } from "@/config/cache.config";
|
||||
import { CalComAPIService, type UserProfile } from "@/services/calcom";
|
||||
|
||||
/**
|
||||
* User profile update input type
|
||||
|
||||
@@ -8,4 +8,4 @@ export {
|
||||
createQueryPersister,
|
||||
getCacheMetadata,
|
||||
storage,
|
||||
} from "../utils/queryPersister";
|
||||
} from "@/utils/queryPersister";
|
||||
@@ -8,4 +8,4 @@ export {
|
||||
isChromeStorageAvailable,
|
||||
type StorageAdapter,
|
||||
secureStorage,
|
||||
} from "../utils/storage";
|
||||
} from "@/utils/storage";
|
||||
@@ -1,4 +1,4 @@
|
||||
const path = require("path");
|
||||
const path = require("node:path");
|
||||
const { getDefaultConfig } = require("expo/metro-config");
|
||||
const { withNativeWind } = require("nativewind/metro");
|
||||
|
||||
|
||||
@@ -66,6 +66,8 @@
|
||||
"expo-dev-client": "6.1.0-canary-20251216-3f01dbf",
|
||||
"expo-device": "8.0.11-canary-20251216-3f01dbf",
|
||||
"expo-glass-effect": "0.2.0-canary-20251216-3f01dbf",
|
||||
"expo-haptics": "^15.0.8",
|
||||
"expo-image": "^3.0.11",
|
||||
"expo-linking": "8.0.11-canary-20251216-3f01dbf",
|
||||
"expo-router": "7.0.0-canary-20251216-3f01dbf",
|
||||
"expo-secure-store": "15.0.9-canary-20251216-3f01dbf",
|
||||
|
||||
@@ -4,7 +4,41 @@ module.exports = {
|
||||
presets: [require("nativewind/preset")],
|
||||
darkMode: "class",
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
cal: {
|
||||
text: {
|
||||
DEFAULT: "#333333",
|
||||
secondary: "#666666",
|
||||
muted: "#8E8E93",
|
||||
emphasis: "#3C3F44",
|
||||
},
|
||||
bg: {
|
||||
DEFAULT: "#FFFFFF",
|
||||
secondary: "#F8F9FA",
|
||||
muted: "#F2F2F7",
|
||||
},
|
||||
border: {
|
||||
DEFAULT: "#E5E5EA",
|
||||
light: "#C6C6C8",
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: "#007AFF",
|
||||
success: "#34C759",
|
||||
warning: "#FF9500",
|
||||
error: "#FF3B30",
|
||||
destructive: "#DC2626",
|
||||
},
|
||||
brand: {
|
||||
DEFAULT: "#292929",
|
||||
black: "#000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
spacing: {
|
||||
18: "4.5rem",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -11,7 +11,18 @@
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"types": ["chrome"]
|
||||
"types": ["chrome"],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@/components/*": ["./components/*"],
|
||||
"@/hooks/*": ["./hooks/*"],
|
||||
"@/utils/*": ["./utils/*"],
|
||||
"@/services/*": ["./services/*"],
|
||||
"@/contexts/*": ["./contexts/*"],
|
||||
"@/config/*": ["./config/*"],
|
||||
"@/types/*": ["./types/*"]
|
||||
}
|
||||
},
|
||||
"include": ["extension/**/*.ts", "extension/**/*.tsx", "services/types/**/*.ts", "types/**/*.ts"],
|
||||
"exclude": ["node_modules", ".output"]
|
||||
|
||||
+12
-1
@@ -1,7 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"types": ["nativewind/types"],
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./*"],
|
||||
"@/components/*": ["./components/*"],
|
||||
"@/hooks/*": ["./hooks/*"],
|
||||
"@/utils/*": ["./utils/*"],
|
||||
"@/services/*": ["./services/*"],
|
||||
"@/contexts/*": ["./contexts/*"],
|
||||
"@/config/*": ["./config/*"],
|
||||
"@/types/*": ["./types/*"]
|
||||
}
|
||||
},
|
||||
"extends": "expo/tsconfig.base",
|
||||
"include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts", "nativewind-env.d.ts"],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Booking } from "../services/types/bookings.types";
|
||||
import type { Booking } from "@/services/types/bookings.types";
|
||||
|
||||
/**
|
||||
* Attendee type extracted from Booking for reuse across the codebase.
|
||||
|
||||
@@ -11,8 +11,8 @@
|
||||
* Deferred:
|
||||
* - report, reroute, reassign, charge_card
|
||||
*/
|
||||
import type { Booking } from "../services/types/bookings.types";
|
||||
import type { EventType } from "../services/types/event-types.types";
|
||||
import type { Booking } from "@/services/types/bookings.types";
|
||||
import type { EventType } from "@/services/types/event-types.types";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { BookingFilter } from "../hooks";
|
||||
import type { Booking } from "../services/calcom";
|
||||
import type { BookingFilter } from "@/hooks";
|
||||
import type { Booking } from "@/services/calcom";
|
||||
|
||||
export const getEmptyStateContent = (activeFilter: BookingFilter) => {
|
||||
switch (activeFilter) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EventType } from "../hooks";
|
||||
import type { EventType } from "@/hooks";
|
||||
|
||||
export const getEventDuration = (eventType: EventType): number => {
|
||||
// Prefer lengthInMinutes (API field), fallback to length for backwards compatibility
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
LocationItem,
|
||||
LocationOption,
|
||||
LocationOptionGroup,
|
||||
} from "../types/locations";
|
||||
} from "@/types/locations";
|
||||
import { DefaultLocationType, defaultLocations } from "./defaultLocations";
|
||||
import { formatAppIdToDisplayName } from "./formatters";
|
||||
import { getAppIconUrl } from "./getAppIconUrl";
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type { PersistedClient, Persister } from "@tanstack/react-query-persist-client";
|
||||
import { CACHE_CONFIG } from "../config/cache.config";
|
||||
import { CACHE_CONFIG } from "@/config/cache.config";
|
||||
import { generalStorage } from "./storage";
|
||||
|
||||
// Use the shared general storage adapter for cache persistence
|
||||
|
||||
Reference in new issue
Block a user