From 9203cb262c103922a995dfec6de1881c34b10af2 Mon Sep 17 00:00:00 2001 From: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Date: Sun, 28 Dec 2025 16:38:33 +0530 Subject: [PATCH] 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. --- companion/app/(tabs)/(availability)/index.tsx | 2 +- .../app/(tabs)/(bookings)/add-guests.tsx | 8 +- .../app/(tabs)/(bookings)/booking-detail.tsx | 8 +- .../app/(tabs)/(bookings)/edit-location.tsx | 8 +- companion/app/(tabs)/(bookings)/index.ios.tsx | 6 +- companion/app/(tabs)/(bookings)/index.tsx | 10 +- .../app/(tabs)/(bookings)/mark-no-show.tsx | 4 +- .../(bookings)/meeting-session-details.tsx | 6 +- .../app/(tabs)/(bookings)/reschedule.tsx | 8 +- .../app/(tabs)/(bookings)/view-recordings.tsx | 6 +- companion/app/(tabs)/(event-types)/index.tsx | 28 +-- companion/app/(tabs)/more.tsx | 10 +- companion/app/_layout.tsx | 8 +- companion/app/availability-detail.tsx | 2 +- companion/app/event-type-detail.tsx | 24 +-- companion/app/oauth/callback.tsx | 2 +- companion/bun.lock | 6 + companion/components/AppPressable.tsx | 110 ++++++++++- companion/components/AuthTokenProvider.tsx | 4 +- .../components/BookingActionsModal.ios.tsx | 4 +- companion/components/BookingActionsModal.tsx | 4 +- companion/components/CacheStatusIndicator.tsx | 2 +- companion/components/Header.tsx | 6 +- companion/components/LocationsList.tsx | 4 +- companion/components/LoginScreen.tsx | 6 +- companion/components/LogoutButton.tsx | 2 +- companion/components/SvgImage.tsx | 8 +- .../AvailabilityListItem.ios.tsx | 41 +--- .../AvailabilityListItem.tsx | 64 ++----- .../AvailabilityListItemParts.tsx | 86 +++++++++ .../booking-list-item/BookingListItem.ios.tsx | 172 ++++------------- .../booking-list-item/BookingListItem.tsx | 171 ++++------------- .../BookingListItemParts.tsx | 179 ++++++++++++++++++ .../components/booking-list-item/types.ts | 2 +- .../useBookingListItemData.ts | 55 ++++++ .../booking-list-screen/BookingListScreen.tsx | 20 +- .../booking-modals/BookingModals.tsx | 9 +- .../event-type-detail/tabs/AdvancedTab.tsx | 2 +- .../tabs/AvailabilityTab.tsx | 2 +- .../event-type-detail/tabs/BasicsTab.tsx | 6 +- .../event-type-detail/tabs/RecurringTab.tsx | 2 +- .../components/event-type-detail/utils.ts | 4 +- .../utils/buildPartialUpdatePayload.ts | 12 +- .../EventTypeListItem.ios.tsx | 57 ++---- .../EventTypeListItem.tsx | 110 +++-------- .../EventTypeListItemParts.tsx | 133 +++++++++++++ .../components/event-type-list-item/types.ts | 2 +- .../useEventTypeListItemData.ts | 32 ++++ .../components/screens/AddGuestsScreen.tsx | 4 +- .../screens/AvailabilityDetailScreen.tsx | 10 +- .../screens/AvailabilityListScreen.tsx | 22 +-- .../screens/BookingDetailScreen.tsx | 22 +-- .../screens/EditLocationScreen.ios.tsx | 4 +- .../components/screens/EditLocationScreen.tsx | 4 +- .../components/screens/MarkNoShowScreen.tsx | 4 +- .../screens/MeetingSessionDetailsScreen.tsx | 2 +- .../components/screens/RescheduleScreen.tsx | 4 +- .../screens/ViewRecordingsScreen.tsx | 2 +- companion/contexts/AuthContext.tsx | 10 +- companion/contexts/QueryContext.tsx | 4 +- companion/hooks/index.ts | 4 +- companion/hooks/useBookingActionModals.ts | 8 +- companion/hooks/useBookingActions.ts | 4 +- companion/hooks/useBookingActionsGating.ts | 6 +- companion/hooks/useBookings.ts | 4 +- companion/hooks/useEventTypes.ts | 4 +- companion/hooks/useSchedules.ts | 4 +- companion/hooks/useUserProfile.ts | 4 +- companion/lib/queryPersister.ts | 2 +- companion/lib/storage.ts | 2 +- companion/metro.config.js | 2 +- companion/package.json | 2 + companion/tailwind.config.js | 36 +++- companion/tsconfig.extension.json | 13 +- companion/tsconfig.json | 13 +- companion/types/attendee.ts | 2 +- companion/utils/booking-actions.ts | 4 +- companion/utils/bookings-utils.ts | 4 +- companion/utils/getEventDuration.ts | 2 +- companion/utils/locationHelpers.ts | 2 +- companion/utils/queryPersister.ts | 2 +- 81 files changed, 1002 insertions(+), 671 deletions(-) create mode 100644 companion/components/availability-list-item/AvailabilityListItemParts.tsx create mode 100644 companion/components/booking-list-item/BookingListItemParts.tsx create mode 100644 companion/components/booking-list-item/useBookingListItemData.ts create mode 100644 companion/components/event-type-list-item/EventTypeListItemParts.tsx create mode 100644 companion/components/event-type-list-item/useEventTypeListItemData.ts diff --git a/companion/app/(tabs)/(availability)/index.tsx b/companion/app/(tabs)/(availability)/index.tsx index 469b0cb3b2..154c747526 100644 --- a/companion/app/(tabs)/(availability)/index.tsx +++ b/companion/app/(tabs)/(availability)/index.tsx @@ -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(""); diff --git a/companion/app/(tabs)/(bookings)/add-guests.tsx b/companion/app/(tabs)/(bookings)/add-guests.tsx index 1fb237d815..6d994f89cb 100644 --- a/companion/app/(tabs)/(bookings)/add-guests.tsx +++ b/companion/app/(tabs)/(bookings)/add-guests.tsx @@ -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 }>(); diff --git a/companion/app/(tabs)/(bookings)/booking-detail.tsx b/companion/app/(tabs)/(bookings)/booking-detail.tsx index 4068930144..243dd89a1e 100644 --- a/companion/app/(tabs)/(bookings)/booking-detail.tsx +++ b/companion/app/(tabs)/(bookings)/booking-detail.tsx @@ -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 = { diff --git a/companion/app/(tabs)/(bookings)/edit-location.tsx b/companion/app/(tabs)/(bookings)/edit-location.tsx index 5b2a093f05..58050595ef 100644 --- a/companion/app/(tabs)/(bookings)/edit-location.tsx +++ b/companion/app/(tabs)/(bookings)/edit-location.tsx @@ -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 }>(); diff --git a/companion/app/(tabs)/(bookings)/index.ios.tsx b/companion/app/(tabs)/(bookings)/index.ios.tsx index fbf9a2eceb..50ce112364 100644 --- a/companion/app/(tabs)/(bookings)/index.ios.tsx +++ b/companion/app/(tabs)/(bookings)/index.ios.tsx @@ -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(""); diff --git a/companion/app/(tabs)/(bookings)/index.tsx b/companion/app/(tabs)/(bookings)/index.tsx index 88b0f129ae..230038de16 100644 --- a/companion/app/(tabs)/(bookings)/index.tsx +++ b/companion/app/(tabs)/(bookings)/index.tsx @@ -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(""); diff --git a/companion/app/(tabs)/(bookings)/mark-no-show.tsx b/companion/app/(tabs)/(bookings)/mark-no-show.tsx index f1b16ffdec..46b40756cf 100644 --- a/companion/app/(tabs)/(bookings)/mark-no-show.tsx +++ b/companion/app/(tabs)/(bookings)/mark-no-show.tsx @@ -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; diff --git a/companion/app/(tabs)/(bookings)/meeting-session-details.tsx b/companion/app/(tabs)/(bookings)/meeting-session-details.tsx index 1f2fa4f722..95143f0fcd 100644 --- a/companion/app/(tabs)/(bookings)/meeting-session-details.tsx +++ b/companion/app/(tabs)/(bookings)/meeting-session-details.tsx @@ -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 }>(); diff --git a/companion/app/(tabs)/(bookings)/reschedule.tsx b/companion/app/(tabs)/(bookings)/reschedule.tsx index 3543dc293c..b1228543e4 100644 --- a/companion/app/(tabs)/(bookings)/reschedule.tsx +++ b/companion/app/(tabs)/(bookings)/reschedule.tsx @@ -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 }>(); diff --git a/companion/app/(tabs)/(bookings)/view-recordings.tsx b/companion/app/(tabs)/(bookings)/view-recordings.tsx index 5f06dbff34..ddd28b5915 100644 --- a/companion/app/(tabs)/(bookings)/view-recordings.tsx +++ b/companion/app/(tabs)/(bookings)/view-recordings.tsx @@ -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 }>(); diff --git a/companion/app/(tabs)/(event-types)/index.tsx b/companion/app/(tabs)/(event-types)/index.tsx index 9804873c3b..608ca9a03f 100644 --- a/companion/app/(tabs)/(event-types)/index.tsx +++ b/companion/app/(tabs)/(event-types)/index.tsx @@ -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(); diff --git a/companion/app/(tabs)/more.tsx b/companion/app/(tabs)/more.tsx index 5900e88cd8..b91788c0e0 100644 --- a/companion/app/(tabs)/more.tsx +++ b/companion/app/(tabs)/more.tsx @@ -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; diff --git a/companion/app/_layout.tsx b/companion/app/_layout.tsx index be2cedffed..3371721fb5 100644 --- a/companion/app/_layout.tsx +++ b/companion/app/_layout.tsx @@ -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() { diff --git a/companion/app/availability-detail.tsx b/companion/app/availability-detail.tsx index 875e16b5fd..88d107038a 100644 --- a/companion/app/availability-detail.tsx +++ b/companion/app/availability-detail.tsx @@ -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 }>(); diff --git a/companion/app/event-type-detail.tsx b/companion/app/event-type-detail.tsx index f33e77aa9c..7f923f57a0 100644 --- a/companion/app/event-type-detail.tsx +++ b/companion/app/event-type-detail.tsx @@ -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 { diff --git a/companion/app/oauth/callback.tsx b/companion/app/oauth/callback.tsx index 2af0a8833c..7fa2a100ff 100644 --- a/companion/app/oauth/callback.tsx +++ b/companion/app/oauth/callback.tsx @@ -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(); diff --git a/companion/bun.lock b/companion/bun.lock index fce7c4088d..beb96608a3 100644 --- a/companion/bun.lock +++ b/companion/bun.lock @@ -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=="], diff --git a/companion/components/AppPressable.tsx b/companion/components/AppPressable.tsx index 23f2be28d8..8daba79044 100644 --- a/companion/components/AppPressable.tsx +++ b/companion/components/AppPressable.tsx @@ -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, - AppPressableProps ->(({ className, ...props }, ref) => { - return ; -}); +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, sc: SharedValue) => { + op.value = withTiming(activeOpacity, { duration: 100 }); + sc.value = withTiming(0.98, { duration: 100 }); + }, + [activeOpacity] + ); + + const animatePressOut = useCallback((op: SharedValue, sc: SharedValue) => { + op.value = withTiming(1, { duration: 100 }); + sc.value = withTiming(1, { duration: 100 }); + }, []); + + return { animatedStyle, animatePressIn, animatePressOut, opacity, scale }; +} + +export const AppPressable = React.forwardRef, 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 ( + + ); + } +); AppPressable.displayName = "AppPressable"; diff --git a/companion/components/AuthTokenProvider.tsx b/companion/components/AuthTokenProvider.tsx index 4d86544e50..e5c3fe1fa9 100644 --- a/companion/components/AuthTokenProvider.tsx +++ b/companion/components/AuthTokenProvider.tsx @@ -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(); diff --git a/companion/components/BookingActionsModal.ios.tsx b/companion/components/BookingActionsModal.ios.tsx index 090776091a..9a5e6c69c0 100644 --- a/companion/components/BookingActionsModal.ios.tsx +++ b/companion/components/BookingActionsModal.ios.tsx @@ -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 { diff --git a/companion/components/BookingActionsModal.tsx b/companion/components/BookingActionsModal.tsx index 81cf9ffc8e..2a7aa2c9a8 100644 --- a/companion/components/BookingActionsModal.tsx +++ b/companion/components/BookingActionsModal.tsx @@ -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 { diff --git a/companion/components/CacheStatusIndicator.tsx b/companion/components/CacheStatusIndicator.tsx index e82104d717..597732848f 100644 --- a/companion/components/CacheStatusIndicator.tsx +++ b/companion/components/CacheStatusIndicator.tsx @@ -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 diff --git a/companion/components/Header.tsx b/companion/components/Header.tsx index ff2aa7b903..81a450a442 100644 --- a/companion/components/Header.tsx +++ b/companion/components/Header.tsx @@ -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"; diff --git a/companion/components/LocationsList.tsx b/companion/components/LocationsList.tsx index b5a83b96f6..8783d1999f 100644 --- a/companion/components/LocationsList.tsx +++ b/companion/components/LocationsList.tsx @@ -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 { diff --git a/companion/components/LoginScreen.tsx b/companion/components/LoginScreen.tsx index 205570ce60..120599d7d7 100644 --- a/companion/components/LoginScreen.tsx +++ b/companion/components/LoginScreen.tsx @@ -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() { diff --git a/companion/components/LogoutButton.tsx b/companion/components/LogoutButton.tsx index c74587417e..02e9889f48 100644 --- a/companion/components/LogoutButton.tsx +++ b/companion/components/LogoutButton.tsx @@ -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 { diff --git a/companion/components/SvgImage.tsx b/companion/components/SvgImage.tsx index dd939ff92a..9d81c29d5b 100644 --- a/companion/components/SvgImage.tsx +++ b/companion/components/SvgImage.tsx @@ -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 = ({ 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 ( { // Image load errors are expected for invalid/missing URLs - silently handled }} diff --git a/companion/components/availability-list-item/AvailabilityListItem.ios.tsx b/companion/components/availability-list-item/AvailabilityListItem.ios.tsx index 20e5beba06..fc4b4471b1 100644 --- a/companion/components/availability-list-item/AvailabilityListItem.ios.tsx +++ b/companion/components/availability-list-item/AvailabilityListItem.ios.tsx @@ -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 ( @@ -55,42 +55,15 @@ export const AvailabilityListItem = ({ accessibilityRole="button" > - - {schedule.name} - {schedule.isDefault && ( - - Default - - )} - - - {schedule.availability && schedule.availability.length > 0 ? ( - - {schedule.availability.map((slot, slotIndex) => ( - 0 ? "mt-2" : ""} - > - - {slot.days.join(", ")} {slot.startTime} - {slot.endTime} - - - ))} - - ) : ( - No availability set - )} - - - - {schedule.timeZone} - + + + {/* Three dots menu - fixed hit target so it doesn't get squeezed off-screen */} diff --git a/companion/components/availability-list-item/AvailabilityListItem.tsx b/companion/components/availability-list-item/AvailabilityListItem.tsx index 8c18763f72..6e5d115921 100644 --- a/companion/components/availability-list-item/AvailabilityListItem.tsx +++ b/companion/components/availability-list-item/AvailabilityListItem.tsx @@ -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 ( handleSchedulePress(schedule)} onLongPress={() => handleScheduleLongPress(schedule)} style={{ paddingHorizontal: 16, paddingVertical: 16 }} > - - {schedule.name} - {schedule.isDefault && ( - - Default - - )} - - - {schedule.availability && schedule.availability.length > 0 ? ( - - {schedule.availability.map((slot, slotIndex) => ( - 0 ? "mt-2" : ""} - > - - {slot.days.join(", ")} {slot.startTime} - {slot.endTime} - - - ))} - - ) : ( - No availability set - )} - - - - {schedule.timeZone} - + + + - - {/* Three dots button - vertically centered on the right */} - { - e.stopPropagation(); - setSelectedSchedule(schedule); - setShowActionsModal(true); - }} - > - - + ); diff --git a/companion/components/availability-list-item/AvailabilityListItemParts.tsx b/companion/components/availability-list-item/AvailabilityListItemParts.tsx new file mode 100644 index 0000000000..8ce5f4edd5 --- /dev/null +++ b/companion/components/availability-list-item/AvailabilityListItemParts.tsx @@ -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 ( + + {name} + {isDefault && ( + + Default + + )} + + ); +} + +interface AvailabilitySlotsProps { + availability?: Schedule["availability"]; + scheduleId: number; +} + +export function AvailabilitySlots({ availability, scheduleId }: AvailabilitySlotsProps) { + if (!availability || availability.length === 0) { + return No availability set; + } + + return ( + + {availability.map((slot, slotIndex) => ( + 0 ? "mt-2" : ""} + > + + {slot.days.join(", ")} {slot.startTime} - {slot.endTime} + + + ))} + + ); +} + +interface TimeZoneRowProps { + timeZone: string; +} + +export function TimeZoneRow({ timeZone }: TimeZoneRowProps) { + return ( + + + {timeZone} + + ); +} + +interface ScheduleActionsButtonProps { + schedule: Schedule; + setSelectedSchedule: (schedule: Schedule) => void; + setShowActionsModal: (show: boolean) => void; +} + +export function ScheduleActionsButton({ + schedule, + setSelectedSchedule, + setShowActionsModal, +}: ScheduleActionsButtonProps) { + return ( + { + e.stopPropagation(); + setSelectedSchedule(schedule); + setShowActionsModal(true); + }} + > + + + ); +} diff --git a/companion/components/booking-list-item/BookingListItem.ios.tsx b/companion/components/booking-list-item/BookingListItem.ios.tsx index 9428582bee..a2c24346c4 100644 --- a/companion/components/booking-list-item/BookingListItem.ios.tsx +++ b/companion/components/booking-list-item/BookingListItem.ios.tsx @@ -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 = ({ booking, @@ -31,19 +36,17 @@ export const BookingListItem: React.FC = ({ 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 = ({ .map(({ label, icon, onPress, role }) => ({ label, icon, onPress, role })); return ( - + 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 */} - - - {formatDate(startTime, isUpcoming)} - - - {formatTime(startTime)} - {formatTime(endTime)} - - - {/* Badges Row */} - - {isPending ? ( - - Unconfirmed - - ) : null} - - {/* Title */} - - {booking.title} - - {/* Description */} - {booking.description ? ( - - "{booking.description}" - - ) : null} - {/* Host and Attendees */} - {hostAndAttendeesDisplay ? ( - - {hostAndAttendeesDisplay} - {hasNoShowAttendee && ( - - - No-show - - )} - - ) : null} - {/* Meeting Link */} - {meetingInfo ? ( - - { - e.stopPropagation(); - try { - await Linking.openURL(meetingInfo.cleanUrl); - } catch { - showErrorAlert("Error", "Failed to open meeting link. Please try again."); - } - }} - > - {meetingInfo.iconUrl ? ( - - ) : ( - - )} - {meetingInfo.label} - - - ) : null} + + + + + + - {isPending ? ( - <> - { - e.stopPropagation(); - onConfirm(booking); - }} - > - - Confirm - - { - e.stopPropagation(); - onReject(booking); - }} - > - - Reject - - - ) : null} + {/* iOS Context Menu */} diff --git a/companion/components/booking-list-item/BookingListItem.tsx b/companion/components/booking-list-item/BookingListItem.tsx index 31af7b3387..95acc66a4d 100644 --- a/companion/components/booking-list-item/BookingListItem.tsx +++ b/companion/components/booking-list-item/BookingListItem.tsx @@ -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 = ({ booking, @@ -18,144 +24,49 @@ export const BookingListItem: React.FC = ({ 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 ( - + onPress(booking)} onLongPress={() => onLongPress(booking)} style={{ paddingHorizontal: 16, paddingTop: 16, paddingBottom: 12 }} > - {/* Time and Date Row */} - - - {formatDate(startTime, isUpcoming)} - - - {formatTime(startTime)} - {formatTime(endTime)} - - - {/* Badges Row */} - - {isPending ? ( - - Unconfirmed - - ) : null} - - {/* Title */} - - {booking.title} - - {/* Description */} - {booking.description ? ( - - "{booking.description}" - - ) : null} - {/* Host and Attendees */} - {hostAndAttendeesDisplay ? ( - - {hostAndAttendeesDisplay} - {hasNoShowAttendee && ( - - - No-show - - )} - - ) : null} - {/* Meeting Link */} - {meetingInfo ? ( - - { - e.stopPropagation(); - try { - await Linking.openURL(meetingInfo.cleanUrl); - } catch { - showErrorAlert("Error", "Failed to open meeting link. Please try again."); - } - }} - > - {meetingInfo.iconUrl ? ( - - ) : ( - - )} - {meetingInfo.label} - - - ) : null} + + + + + + - {isPending ? ( - <> - { - e.stopPropagation(); - onConfirm(booking); - }} - > - - Confirm - - { - e.stopPropagation(); - onReject(booking); - }} - > - - Reject - - - ) : null} + { e.stopPropagation(); diff --git a/companion/components/booking-list-item/BookingListItemParts.tsx b/companion/components/booking-list-item/BookingListItemParts.tsx new file mode 100644 index 0000000000..26032091d5 --- /dev/null +++ b/companion/components/booking-list-item/BookingListItemParts.tsx @@ -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 ( + + {formattedDate} + {formattedTimeRange} + + ); +} + +interface BadgesRowProps { + isPending: boolean; +} + +export function BadgesRow({ isPending }: BadgesRowProps) { + return ( + + {isPending ? ( + + Unconfirmed + + ) : null} + + ); +} + +interface BookingTitleProps { + title: string; + isCancelled: boolean; + isRejected: boolean; +} + +export function BookingTitle({ title, isCancelled, isRejected }: BookingTitleProps) { + return ( + + {title} + + ); +} + +interface BookingDescriptionProps { + description?: string | null; +} + +export function BookingDescription({ description }: BookingDescriptionProps) { + if (!description) return null; + return ( + + "{description}" + + ); +} + +interface HostAndAttendeesProps { + hostAndAttendeesDisplay: string | null; + hasNoShowAttendee: boolean; +} + +export function HostAndAttendees({ + hostAndAttendeesDisplay, + hasNoShowAttendee, +}: HostAndAttendeesProps) { + if (!hostAndAttendeesDisplay) return null; + return ( + + {hostAndAttendeesDisplay} + {hasNoShowAttendee && ( + + + + No-show + + + )} + + ); +} + +interface MeetingLinkProps { + meetingInfo: BookingListItemData["meetingInfo"]; +} + +export function MeetingLink({ meetingInfo }: MeetingLinkProps) { + if (!meetingInfo) return null; + return ( + + { + e.stopPropagation(); + try { + await Linking.openURL(meetingInfo.cleanUrl); + } catch { + showErrorAlert("Error", "Failed to open meeting link. Please try again."); + } + }} + > + {meetingInfo.iconUrl ? ( + + ) : ( + + )} + {meetingInfo.label} + + + ); +} + +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 ( + <> + { + e.stopPropagation(); + onConfirm(booking); + }} + > + + Confirm + + { + e.stopPropagation(); + onReject(booking); + }} + > + + Reject + + + ); +} diff --git a/companion/components/booking-list-item/types.ts b/companion/components/booking-list-item/types.ts index a563edc1a0..f1abd3d1db 100644 --- a/companion/components/booking-list-item/types.ts +++ b/companion/components/booking-list-item/types.ts @@ -1,4 +1,4 @@ -import type { Booking } from "../../services/calcom"; +import type { Booking } from "@/services/calcom"; export interface BookingListItemProps { booking: Booking; diff --git a/companion/components/booking-list-item/useBookingListItemData.ts b/companion/components/booking-list-item/useBookingListItemData.ts new file mode 100644 index 0000000000..69901ced0c --- /dev/null +++ b/companion/components/booking-list-item/useBookingListItemData.ts @@ -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; + 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, + }; +} diff --git a/companion/components/booking-list-screen/BookingListScreen.tsx b/companion/components/booking-list-screen/BookingListScreen.tsx index 05309ed918..ee26a85480 100644 --- a/companion/components/booking-list-screen/BookingListScreen.tsx +++ b/companion/components/booking-list-screen/BookingListScreen.tsx @@ -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 diff --git a/companion/components/booking-modals/BookingModals.tsx b/companion/components/booking-modals/BookingModals.tsx index dd525f0f30..0846be12a8 100644 --- a/companion/components/booking-modals/BookingModals.tsx +++ b/companion/components/booking-modals/BookingModals.tsx @@ -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 = { diff --git a/companion/components/event-type-detail/tabs/AdvancedTab.tsx b/companion/components/event-type-detail/tabs/AdvancedTab.tsx index 8eedd9df4a..6170428589 100644 --- a/companion/components/event-type-detail/tabs/AdvancedTab.tsx +++ b/companion/components/event-type-detail/tabs/AdvancedTab.tsx @@ -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; diff --git a/companion/components/event-type-detail/tabs/AvailabilityTab.tsx b/companion/components/event-type-detail/tabs/AvailabilityTab.tsx index e3dc49ae99..c07e4117cb 100644 --- a/companion/components/event-type-detail/tabs/AvailabilityTab.tsx +++ b/companion/components/event-type-detail/tabs/AvailabilityTab.tsx @@ -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; diff --git a/companion/components/event-type-detail/tabs/BasicsTab.tsx b/companion/components/event-type-detail/tabs/BasicsTab.tsx index 7b72fdc980..fb23471571 100644 --- a/companion/components/event-type-detail/tabs/BasicsTab.tsx +++ b/companion/components/event-type-detail/tabs/BasicsTab.tsx @@ -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 diff --git a/companion/components/event-type-detail/tabs/RecurringTab.tsx b/companion/components/event-type-detail/tabs/RecurringTab.tsx index db3b982ac1..4240fb9b9d 100644 --- a/companion/components/event-type-detail/tabs/RecurringTab.tsx +++ b/companion/components/event-type-detail/tabs/RecurringTab.tsx @@ -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; diff --git a/companion/components/event-type-detail/utils.ts b/companion/components/event-type-detail/utils.ts index 4ce9d780db..a8124655f5 100644 --- a/companion/components/event-type-detail/utils.ts +++ b/companion/components/event-type-detail/utils.ts @@ -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"; diff --git a/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts b/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts index 5146803958..8b6f0c4dc2 100644 --- a/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts +++ b/companion/components/event-type-detail/utils/buildPartialUpdatePayload.ts @@ -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; diff --git a/companion/components/event-type-list-item/EventTypeListItem.ios.tsx b/companion/components/event-type-list-item/EventTypeListItem.ios.tsx index dbfbb00905..86222b3d24 100644 --- a/companion/components/event-type-list-item/EventTypeListItem.ios.tsx +++ b/companion/components/event-type-list-item/EventTypeListItem.ios.tsx @@ -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["systemImage"]; @@ -67,7 +71,9 @@ export const EventTypeListItem = ({ ]; return ( - + handleEventTypePress(item)} @@ -75,35 +81,14 @@ export const EventTypeListItem = ({ className="flex-grow" > - - {item.title} - - {item.description ? ( - - {normalizeMarkdown(item.description)} - - ) : null} - - - - {formatDuration(duration)} - - - {(item.price != null && item.price > 0) || item.requiresConfirmation ? ( - - {item.price != null && item.price > 0 ? ( - - {item.currency || "$"} - {item.price} - - ) : null} - {item.requiresConfirmation ? ( - - Requires Confirmation - - ) : null} - - ) : null} + + + + diff --git a/companion/components/event-type-list-item/EventTypeListItem.tsx b/companion/components/event-type-list-item/EventTypeListItem.tsx index 2cb8af5e78..4a10c9a48f 100644 --- a/companion/components/event-type-list-item/EventTypeListItem.tsx +++ b/companion/components/event-type-list-item/EventTypeListItem.tsx @@ -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 ( handleEventTypePress(item)} onLongPress={() => handleEventTypeLongPress(item)} style={{ paddingHorizontal: 16, paddingVertical: 16 }} > - - {item.title} - - {item.description ? ( - - {normalizeMarkdown(item.description)} - - ) : null} - - - - {formatDuration(duration)} - - - {(item.price != null && item.price > 0) || item.requiresConfirmation ? ( - - {item.price != null && item.price > 0 ? ( - - {item.currency || "$"} - {item.price} - - ) : null} - {item.requiresConfirmation ? ( - - Requires Confirmation - - ) : null} - - ) : null} - - - - handlePreview(item)} - > - - - - - handleCopyLink(item)} - > - {copiedEventTypeId === item.id ? ( - - ) : ( - - - - - )} - - - - handleEventTypeLongPress(item)} - > - - - + + + + + ); diff --git a/companion/components/event-type-list-item/EventTypeListItemParts.tsx b/companion/components/event-type-list-item/EventTypeListItemParts.tsx new file mode 100644 index 0000000000..83e76bf917 --- /dev/null +++ b/companion/components/event-type-list-item/EventTypeListItemParts.tsx @@ -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 ( + + {title} + + ); +} + +interface EventTypeDescriptionProps { + normalizedDescription: string | null; +} + +export function EventTypeDescription({ normalizedDescription }: EventTypeDescriptionProps) { + if (!normalizedDescription) return null; + return ( + + {normalizedDescription} + + ); +} + +interface DurationBadgeProps { + formattedDuration: string; +} + +export function DurationBadge({ formattedDuration }: DurationBadgeProps) { + return ( + + + {formattedDuration} + + ); +} + +interface PriceAndConfirmationBadgesProps { + hasPrice: boolean; + formattedPrice: string | null; + requiresConfirmation?: boolean; +} + +export function PriceAndConfirmationBadges({ + hasPrice, + formattedPrice, + requiresConfirmation, +}: PriceAndConfirmationBadgesProps) { + if (!hasPrice && !requiresConfirmation) return null; + return ( + + {hasPrice && formattedPrice ? ( + {formattedPrice} + ) : null} + {requiresConfirmation ? ( + + Requires Confirmation + + ) : null} + + ); +} + +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 ( + + + handlePreview(item)} + > + + + + + handleCopyLink(item)} + > + {copiedEventTypeId === item.id ? ( + + ) : ( + + + + + )} + + + + handleEventTypeLongPress(item)} + > + + + + + ); +} diff --git a/companion/components/event-type-list-item/types.ts b/companion/components/event-type-list-item/types.ts index 786c69a324..a344e9b2b1 100644 --- a/companion/components/event-type-list-item/types.ts +++ b/companion/components/event-type-list-item/types.ts @@ -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; diff --git a/companion/components/event-type-list-item/useEventTypeListItemData.ts b/companion/components/event-type-list-item/useEventTypeListItemData.ts new file mode 100644 index 0000000000..c5dcccd573 --- /dev/null +++ b/companion/components/event-type-list-item/useEventTypeListItemData.ts @@ -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, + }; +} diff --git a/companion/components/screens/AddGuestsScreen.tsx b/companion/components/screens/AddGuestsScreen.tsx index e99e90672a..2a080d04c4 100644 --- a/companion/components/screens/AddGuestsScreen.tsx +++ b/companion/components/screens/AddGuestsScreen.tsx @@ -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; diff --git a/companion/components/screens/AvailabilityDetailScreen.tsx b/companion/components/screens/AvailabilityDetailScreen.tsx index 00694fee56..ca1a76db86 100644 --- a/companion/components/screens/AvailabilityDetailScreen.tsx +++ b/companion/components/screens/AvailabilityDetailScreen.tsx @@ -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"]; diff --git a/companion/components/screens/AvailabilityListScreen.tsx b/companion/components/screens/AvailabilityListScreen.tsx index 82ac10e1c7..1c8475e1b1 100644 --- a/companion/components/screens/AvailabilityListScreen.tsx +++ b/companion/components/screens/AvailabilityListScreen.tsx @@ -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; diff --git a/companion/components/screens/BookingDetailScreen.tsx b/companion/components/screens/BookingDetailScreen.tsx index dad5464ae3..5d8896b33d 100644 --- a/companion/components/screens/BookingDetailScreen.tsx +++ b/companion/components/screens/BookingDetailScreen.tsx @@ -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 = { diff --git a/companion/components/screens/EditLocationScreen.ios.tsx b/companion/components/screens/EditLocationScreen.ios.tsx index 42a26d2613..dc046e7d7c 100644 --- a/companion/components/screens/EditLocationScreen.ios.tsx +++ b/companion/components/screens/EditLocationScreen.ios.tsx @@ -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 = { diff --git a/companion/components/screens/EditLocationScreen.tsx b/companion/components/screens/EditLocationScreen.tsx index 55b699d01b..1598672266 100644 --- a/companion/components/screens/EditLocationScreen.tsx +++ b/companion/components/screens/EditLocationScreen.tsx @@ -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: { diff --git a/companion/components/screens/MarkNoShowScreen.tsx b/companion/components/screens/MarkNoShowScreen.tsx index 7c63cdb9af..4970d40238 100644 --- a/companion/components/screens/MarkNoShowScreen.tsx +++ b/companion/components/screens/MarkNoShowScreen.tsx @@ -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; diff --git a/companion/components/screens/MeetingSessionDetailsScreen.tsx b/companion/components/screens/MeetingSessionDetailsScreen.tsx index c003054bdb..a9bc3b04b2 100644 --- a/companion/components/screens/MeetingSessionDetailsScreen.tsx +++ b/companion/components/screens/MeetingSessionDetailsScreen.tsx @@ -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[]; diff --git a/companion/components/screens/RescheduleScreen.tsx b/companion/components/screens/RescheduleScreen.tsx index 33d83dee7c..f8866ec72e 100644 --- a/companion/components/screens/RescheduleScreen.tsx +++ b/companion/components/screens/RescheduleScreen.tsx @@ -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 diff --git a/companion/components/screens/ViewRecordingsScreen.tsx b/companion/components/screens/ViewRecordingsScreen.tsx index cd6e267351..8947d47f4e 100644 --- a/companion/components/screens/ViewRecordingsScreen.tsx +++ b/companion/components/screens/ViewRecordingsScreen.tsx @@ -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[]; diff --git a/companion/contexts/AuthContext.tsx b/companion/contexts/AuthContext.tsx index a3c8ba0fd3..d24f7df3fb 100644 --- a/companion/contexts/AuthContext.tsx +++ b/companion/contexts/AuthContext.tsx @@ -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 diff --git a/companion/contexts/QueryContext.tsx b/companion/contexts/QueryContext.tsx index d39dfbd9ae..5bba2fd620 100644 --- a/companion/contexts/QueryContext.tsx +++ b/companion/contexts/QueryContext.tsx @@ -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 diff --git a/companion/hooks/index.ts b/companion/hooks/index.ts index 11758a7a74..dcfdfc88de 100644 --- a/companion/hooks/index.ts +++ b/companion/hooks/index.ts @@ -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, diff --git a/companion/hooks/useBookingActionModals.ts b/companion/hooks/useBookingActionModals.ts index d3d29afafd..f6c79bbede 100644 --- a/companion/hooks/useBookingActionModals.ts +++ b/companion/hooks/useBookingActionModals.ts @@ -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 diff --git a/companion/hooks/useBookingActions.ts b/companion/hooks/useBookingActions.ts index 34c6ad7e00..fdee52d6a5 100644 --- a/companion/hooks/useBookingActions.ts +++ b/companion/hooks/useBookingActions.ts @@ -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; diff --git a/companion/hooks/useBookingActionsGating.ts b/companion/hooks/useBookingActionsGating.ts index c2c69c62d0..eb8db2e1b2 100644 --- a/companion/hooks/useBookingActionsGating.ts +++ b/companion/hooks/useBookingActionsGating.ts @@ -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; diff --git a/companion/hooks/useBookings.ts b/companion/hooks/useBookings.ts index 49a07e59b9..1e2569aa6d 100644 --- a/companion/hooks/useBookings.ts +++ b/companion/hooks/useBookings.ts @@ -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 diff --git a/companion/hooks/useEventTypes.ts b/companion/hooks/useEventTypes.ts index 658397aa9e..b97e303409 100644 --- a/companion/hooks/useEventTypes.ts +++ b/companion/hooks/useEventTypes.ts @@ -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 diff --git a/companion/hooks/useSchedules.ts b/companion/hooks/useSchedules.ts index d040d08873..a8a084dc59 100644 --- a/companion/hooks/useSchedules.ts +++ b/companion/hooks/useSchedules.ts @@ -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 diff --git a/companion/hooks/useUserProfile.ts b/companion/hooks/useUserProfile.ts index 9b921143d6..8ebd76b8ae 100644 --- a/companion/hooks/useUserProfile.ts +++ b/companion/hooks/useUserProfile.ts @@ -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 diff --git a/companion/lib/queryPersister.ts b/companion/lib/queryPersister.ts index cc45bedda1..fe170ca44d 100644 --- a/companion/lib/queryPersister.ts +++ b/companion/lib/queryPersister.ts @@ -8,4 +8,4 @@ export { createQueryPersister, getCacheMetadata, storage, -} from "../utils/queryPersister"; +} from "@/utils/queryPersister"; diff --git a/companion/lib/storage.ts b/companion/lib/storage.ts index aaa9a6dd39..2f726b5b3a 100644 --- a/companion/lib/storage.ts +++ b/companion/lib/storage.ts @@ -8,4 +8,4 @@ export { isChromeStorageAvailable, type StorageAdapter, secureStorage, -} from "../utils/storage"; +} from "@/utils/storage"; diff --git a/companion/metro.config.js b/companion/metro.config.js index f04c704ee9..c2ea77351d 100644 --- a/companion/metro.config.js +++ b/companion/metro.config.js @@ -1,4 +1,4 @@ -const path = require("path"); +const path = require("node:path"); const { getDefaultConfig } = require("expo/metro-config"); const { withNativeWind } = require("nativewind/metro"); diff --git a/companion/package.json b/companion/package.json index a1ae8ecb24..d6ce613b56 100644 --- a/companion/package.json +++ b/companion/package.json @@ -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", diff --git a/companion/tailwind.config.js b/companion/tailwind.config.js index bdcfdd978a..d892732c4e 100644 --- a/companion/tailwind.config.js +++ b/companion/tailwind.config.js @@ -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: [], }; diff --git a/companion/tsconfig.extension.json b/companion/tsconfig.extension.json index 9041e5ac6d..74a5a2b5dd 100644 --- a/companion/tsconfig.extension.json +++ b/companion/tsconfig.extension.json @@ -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"] diff --git a/companion/tsconfig.json b/companion/tsconfig.json index f3574c4672..38e5b82283 100644 --- a/companion/tsconfig.json +++ b/companion/tsconfig.json @@ -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"], diff --git a/companion/types/attendee.ts b/companion/types/attendee.ts index 04949e8834..80350a23bd 100644 --- a/companion/types/attendee.ts +++ b/companion/types/attendee.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. diff --git a/companion/utils/booking-actions.ts b/companion/utils/booking-actions.ts index a5eb99ff8d..2fea3b07e4 100644 --- a/companion/utils/booking-actions.ts +++ b/companion/utils/booking-actions.ts @@ -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 diff --git a/companion/utils/bookings-utils.ts b/companion/utils/bookings-utils.ts index 5b2bc24a96..e0f7f29407 100644 --- a/companion/utils/bookings-utils.ts +++ b/companion/utils/bookings-utils.ts @@ -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) { diff --git a/companion/utils/getEventDuration.ts b/companion/utils/getEventDuration.ts index e67955232c..0657af55f5 100644 --- a/companion/utils/getEventDuration.ts +++ b/companion/utils/getEventDuration.ts @@ -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 diff --git a/companion/utils/locationHelpers.ts b/companion/utils/locationHelpers.ts index 537ae3a096..bf072553ff 100644 --- a/companion/utils/locationHelpers.ts +++ b/companion/utils/locationHelpers.ts @@ -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"; diff --git a/companion/utils/queryPersister.ts b/companion/utils/queryPersister.ts index ce5b77fcab..d263b74e9d 100644 --- a/companion/utils/queryPersister.ts +++ b/companion/utils/queryPersister.ts @@ -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