diff --git a/companion/.prettierrc.js b/companion/.prettierrc.js deleted file mode 100644 index 359fced2da..0000000000 --- a/companion/.prettierrc.js +++ /dev/null @@ -1,10 +0,0 @@ -module.exports = { - semi: true, - trailingComma: "es5", - singleQuote: false, - printWidth: 100, - tabWidth: 2, - useTabs: false, - arrowParens: "always", - plugins: ["prettier-plugin-tailwindcss"], -}; diff --git a/companion/api/server.ts b/companion/api/server.ts index 294b211077..4d07f5f9c7 100644 --- a/companion/api/server.ts +++ b/companion/api/server.ts @@ -1,7 +1,6 @@ -import { createReadStream, existsSync } from "fs"; -import { promises as fs } from "fs"; -import { IncomingMessage, ServerResponse } from "http"; -import { extname, join, normalize } from "path"; +import { createReadStream, existsSync, promises as fs } from "node:fs"; +import type { IncomingMessage, ServerResponse } from "node:http"; +import { extname, join, normalize } from "node:path"; const distDir = join(process.cwd(), "dist"); diff --git a/companion/app/(tabs)/(availability)/index.tsx b/companion/app/(tabs)/(availability)/index.tsx index 95444e6d91..469b0cb3b2 100644 --- a/companion/app/(tabs)/(availability)/index.tsx +++ b/companion/app/(tabs)/(availability)/index.tsx @@ -1,8 +1,8 @@ -import { AvailabilityListScreen } from "../../../components/screens/AvailabilityListScreen"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { Stack } from "expo-router"; -import React, { useState } from "react"; +import { useState } from "react"; import { Platform } from "react-native"; +import { AvailabilityListScreen } from "../../../components/screens/AvailabilityListScreen"; export default function Availability() { const [searchQuery, setSearchQuery] = useState(""); diff --git a/companion/app/(tabs)/(bookings)/_layout.tsx b/companion/app/(tabs)/(bookings)/_layout.tsx index 77a440612d..0eef26ba20 100644 --- a/companion/app/(tabs)/(bookings)/_layout.tsx +++ b/companion/app/(tabs)/(bookings)/_layout.tsx @@ -4,7 +4,7 @@ import { Platform } from "react-native"; export default function BookingsLayout() { return ( - + diff --git a/companion/app/(tabs)/(bookings)/add-guests.tsx b/companion/app/(tabs)/(bookings)/add-guests.tsx index 4fc93c3f8c..1fb237d815 100644 --- a/companion/app/(tabs)/(bookings)/add-guests.tsx +++ b/companion/app/(tabs)/(bookings)/add-guests.tsx @@ -1,10 +1,10 @@ -import { AppPressable } from "../../../components/AppPressable"; -import AddGuestsScreen from "../../../components/screens/AddGuestsScreen"; -import type { AddGuestsScreenHandle } from "../../../components/screens/AddGuestsScreen"; -import { CalComAPIService, type Booking } from "../../../services/calcom"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import React, { useState, useEffect, useCallback, useRef } from "react"; -import { Alert, ActivityIndicator, View, Text, Platform } from "react-native"; +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"; export default function AddGuests() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -99,7 +99,7 @@ export default function AddGuests() { )} - { if (!booking) return EMPTY_ACTIONS; diff --git a/companion/app/(tabs)/(bookings)/edit-location.tsx b/companion/app/(tabs)/(bookings)/edit-location.tsx index 8eeee16237..5b2a093f05 100644 --- a/companion/app/(tabs)/(bookings)/edit-location.tsx +++ b/companion/app/(tabs)/(bookings)/edit-location.tsx @@ -1,10 +1,10 @@ -import { AppPressable } from "../../../components/AppPressable"; -import EditLocationScreen from "../../../components/screens/EditLocationScreen"; -import type { EditLocationScreenHandle } from "../../../components/screens/EditLocationScreen"; -import { CalComAPIService, type Booking } from "../../../services/calcom"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import React, { useState, useEffect, useCallback, useRef } from "react"; -import { Alert, ActivityIndicator, View, Text, Platform } from "react-native"; +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"; export default function EditLocation() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -99,7 +99,7 @@ export default function EditLocation() { )} - (); const router = useRouter(); @@ -27,7 +35,7 @@ export default function MarkNoShow() { // API may return "absent" or "noShow" depending on endpoint version const bookingAttendees: Attendee[] = []; if (bookingData.attendees && Array.isArray(bookingData.attendees)) { - bookingData.attendees.forEach((att: any) => { + bookingData.attendees.forEach((att: BookingAttendee) => { bookingAttendees.push({ id: att.id, email: att.email, @@ -90,7 +98,7 @@ export default function MarkNoShow() { )} - { + updatedBooking.attendees.forEach((att: BookingAttendee) => { updatedAttendees.push({ id: att.id, email: att.email, diff --git a/companion/app/(tabs)/(bookings)/meeting-session-details.tsx b/companion/app/(tabs)/(bookings)/meeting-session-details.tsx index 2c17d402ff..1f2fa4f722 100644 --- a/companion/app/(tabs)/(bookings)/meeting-session-details.tsx +++ b/companion/app/(tabs)/(bookings)/meeting-session-details.tsx @@ -1,29 +1,21 @@ -import MeetingSessionDetailsScreen from "../../../components/screens/MeetingSessionDetailsScreen"; -import { CalComAPIService, type Booking } from "../../../services/calcom"; -import type { ConferencingSession } from "../../../services/types/bookings.types"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import React, { useState, useEffect } from "react"; -import { Alert, ActivityIndicator, View, Platform } from "react-native"; +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"; export default function MeetingSessionDetails() { const { uid } = useLocalSearchParams<{ uid: string }>(); const router = useRouter(); - const [booking, setBooking] = useState(null); const [sessions, setSessions] = useState([]); const [isLoading, setIsLoading] = useState(true); useEffect(() => { if (uid) { setIsLoading(true); - Promise.all([ - CalComAPIService.getBookingByUid(uid), - CalComAPIService.getConferencingSessions(uid).catch((error) => { - console.error("Failed to load sessions:", error); - return []; - }), - ]) - .then(([bookingData, sessionsData]) => { - setBooking(bookingData); + CalComAPIService.getConferencingSessions(uid) + .then((sessionsData) => { setSessions(sessionsData); }) .catch(() => { @@ -78,7 +70,7 @@ export default function MeetingSessionDetails() { )} - + ); } diff --git a/companion/app/(tabs)/(bookings)/reschedule.tsx b/companion/app/(tabs)/(bookings)/reschedule.tsx index 280e17cfcb..3543dc293c 100644 --- a/companion/app/(tabs)/(bookings)/reschedule.tsx +++ b/companion/app/(tabs)/(bookings)/reschedule.tsx @@ -1,10 +1,10 @@ -import { AppPressable } from "../../../components/AppPressable"; -import RescheduleScreen from "../../../components/screens/RescheduleScreen"; -import type { RescheduleScreenHandle } from "../../../components/screens/RescheduleScreen"; -import { CalComAPIService, type Booking } from "../../../services/calcom"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import React, { useState, useEffect, useCallback, useRef } from "react"; -import { Alert, ActivityIndicator, View, Text, Platform } from "react-native"; +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"; export default function Reschedule() { const { uid } = useLocalSearchParams<{ uid: string }>(); @@ -98,7 +98,7 @@ export default function Reschedule() { )} - (); @@ -69,7 +69,7 @@ export default function ViewRecordings() { )} - + ); } diff --git a/companion/app/(tabs)/(event-types)/index.tsx b/companion/app/(tabs)/(event-types)/index.tsx index c7d52939bd..a640f47822 100644 --- a/companion/app/(tabs)/(event-types)/index.tsx +++ b/companion/app/(tabs)/(event-types)/index.tsx @@ -1,16 +1,33 @@ +import { Ionicons } from "@expo/vector-icons"; +import * as Clipboard from "expo-clipboard"; +import { isLiquidGlassAvailable } from "expo-glass-effect"; +import { Stack, useRouter } from "expo-router"; +import { useMemo, useState } from "react"; +import { + ActionSheetIOS, + Alert, + Platform, + RefreshControl, + ScrollView, + Share, + Text, + TextInput, + 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 { EventTypeListItem } from "../../../components/event-type-list-item/EventTypeListItem"; import { - useEventTypes, useCreateEventType, useDeleteEventType, useDuplicateEventType, + useEventTypes, useUsername, } from "../../../hooks"; -import { CalComAPIService, EventType } from "../../../services/calcom"; +import { CalComAPIService, type EventType } from "../../../services/calcom"; import { showErrorAlert } from "../../../utils/alerts"; import { openInAppBrowser } from "../../../utils/browser"; import { getEventDuration } from "../../../utils/getEventDuration"; @@ -18,23 +35,6 @@ import { offlineAwareRefresh } from "../../../utils/network"; import { normalizeMarkdown } from "../../../utils/normalizeMarkdown"; import { shadows } from "../../../utils/shadows"; import { slugify } from "../../../utils/slugify"; -import { Ionicons } from "@expo/vector-icons"; -import * as Clipboard from "expo-clipboard"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; -import { Stack, useRouter } from "expo-router"; -import React, { useState, useMemo } from "react"; -import { - View, - Text, - ScrollView, - TouchableOpacity, - RefreshControl, - TextInput, - ActionSheetIOS, - Share, - Alert, - Platform, -} from "react-native"; export default function EventTypes() { const router = useRouter(); @@ -116,7 +116,7 @@ export default function EventTypes() { return eventTypes.filter( (eventType) => eventType.title.toLowerCase().includes(searchLower) || - (eventType.description && eventType.description.toLowerCase().includes(searchLower)) + eventType.description?.toLowerCase().includes(searchLower) ); }, [eventTypes, searchQuery]); @@ -184,7 +184,7 @@ export default function EventTypes() { } else { Alert.alert("Link Copied", "Event type link copied!"); } - } catch (error) { + } catch { if (Platform.OS === "web") { showToastMessage("Failed to copy link"); } else { @@ -193,14 +193,15 @@ export default function EventTypes() { } }; - const handleShare = async (eventType: EventType) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _handleShare = async (eventType: EventType) => { try { const link = await CalComAPIService.buildEventTypeLink(eventType.slug); await Share.share({ message: `Book a meeting: ${eventType.title}`, url: link, }); - } catch (error) { + } catch { showErrorAlert("Error", "Failed to share link. Please try again."); } }; @@ -313,7 +314,7 @@ export default function EventTypes() { // For mobile, use in-app browser await openInAppBrowser(link, "event type preview"); } - } catch (error) { + } catch { console.error("Failed to open preview"); if (Platform.OS === "web") { showToastMessage("Failed to open preview"); @@ -351,8 +352,8 @@ export default function EventTypes() { return; } - const duration = parseInt(newEventDuration); - if (isNaN(duration) || duration <= 0) { + const duration = parseInt(newEventDuration, 10); + if (Number.isNaN(duration) || duration <= 0) { Alert.alert("Error", "Please enter a valid duration"); return; } diff --git a/companion/app/(tabs)/more.tsx b/companion/app/(tabs)/more.tsx index ec80e144f2..5900e88cd8 100644 --- a/companion/app/(tabs)/more.tsx +++ b/companion/app/(tabs)/more.tsx @@ -1,12 +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 { Ionicons } from "@expo/vector-icons"; -import { useRouter } from "expo-router"; -import React, { useState } from "react"; -import { View, Text, TouchableOpacity, ScrollView, Alert, Platform } from "react-native"; interface MoreMenuItem { name: string; @@ -17,7 +16,6 @@ interface MoreMenuItem { } export default function More() { - const router = useRouter(); const { logout } = useAuth(); const [showLogoutModal, setShowLogoutModal] = useState(false); diff --git a/companion/app/_layout.tsx b/companion/app/_layout.tsx index 268eb25824..be2cedffed 100644 --- a/companion/app/_layout.tsx +++ b/companion/app/_layout.tsx @@ -1,9 +1,9 @@ import { Stack } from "expo-router"; -import { Platform, View, StatusBar } from "react-native"; +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 { NetworkStatusBanner } from "../components/NetworkStatusBanner"; -import LoginScreen from "../components/LoginScreen"; import "../global.css"; function RootLayoutContent() { @@ -14,7 +14,7 @@ function RootLayoutContent() { ) : ( - + ); const containerClass = diff --git a/companion/app/availability-detail.tsx b/companion/app/availability-detail.tsx index 65bd0629be..875e16b5fd 100644 --- a/companion/app/availability-detail.tsx +++ b/companion/app/availability-detail.tsx @@ -1,6 +1,5 @@ +import { Stack, useLocalSearchParams } from "expo-router"; import { AvailabilityDetailScreen } from "../components/screens/AvailabilityDetailScreen"; -import { useLocalSearchParams, Stack } from "expo-router"; -import React from "react"; 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 badb2df0f6..dfebac5542 100644 --- a/companion/app/event-type-detail.tsx +++ b/companion/app/event-type-detail.tsx @@ -1,55 +1,112 @@ +import { Ionicons } from "@expo/vector-icons"; +import * as Clipboard from "expo-clipboard"; +import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; +import { Stack, useLocalSearchParams, useRouter } from "expo-router"; +import { useCallback, useEffect, useState } from "react"; +import { + Alert, + Animated, + Modal, + Platform, + ScrollView, + Switch, + Text, + TouchableOpacity, + 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 { - formatDuration, - truncateTitle, - formatAppIdToDisplayName, -} from "../components/event-type-detail/utils"; -import { - buildPartialUpdatePayload, - hasChanges, -} from "../components/event-type-detail/utils/buildPartialUpdatePayload"; -import { CalComAPIService, Schedule, ConferencingOption, EventType } from "../services/calcom"; -import { LocationItem, LocationOptionGroup } from "../types/locations"; + 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"; import { - parseBufferTime, - parseMinimumNotice, - parseFrequencyUnit, - parseSlotInterval, -} from "../utils/eventTypeParsers"; -import { + buildLocationOptions, mapApiLocationToItem, mapItemToApiLocation, - buildLocationOptions, validateLocationItem, } from "../utils/locationHelpers"; -import { slugify } from "../utils/slugify"; -import { Ionicons } from "@expo/vector-icons"; -import * as Clipboard from "expo-clipboard"; -import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; -import { useRouter, useLocalSearchParams, Stack } from "expo-router"; -import React, { useState, useEffect } from "react"; -import { - View, - Text, - ScrollView, - TouchableOpacity, - TextInput, - Switch, - Modal, - Alert, - Animated, - Image, - Platform, -} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -const tabs = [ +// Type definitions for extended EventType fields not in the base type +interface EventTypeExtended { + lengthInMinutesOptions?: number[]; + disableCancelling?: boolean; + disableRescheduling?: boolean; + sendCalVideoTranscription?: boolean; + autoTranslate?: boolean; + lockedTimeZone?: string; + hideCalendarEventDetails?: boolean; + hideOrganizerEmail?: boolean; + color?: { + lightThemeHex?: string; + darkThemeHex?: string; + }; +} + +interface BookerActiveBookingsLimitExtended { + disabled?: boolean; + maximumActiveBookings?: number; + count?: number; + offerReschedule?: boolean; +} + +interface ConfirmationPolicyExtended { + disabled?: boolean; +} + +interface RecurrenceExtended { + disabled?: boolean; + interval?: number; + frequency?: string; + occurrences?: number; +} + +interface SeatsExtended { + disabled?: boolean; + seatsPerTimeSlot?: number; + showAttendeeInfo?: boolean; + showAvailabilityCount?: boolean; +} + +interface ApiLocation { + type: string; + integration?: string; + address?: string; + link?: string; + phone?: string; + public?: boolean; +} + +interface CreateEventTypePayload { + title: string; + slug: string; + lengthInMinutes: number; + description?: string; + locations?: ReturnType[]; + scheduleId?: number; + hidden?: boolean; +} + +type TabIconName = "link" | "calendar" | "time" | "settings" | "refresh" | "ellipsis-horizontal"; + +interface Tab { + id: string; + label: string; + icon: TabIconName; +} + +const tabs: Tab[] = [ { id: "basics", label: "Basics", icon: "link" }, { id: "availability", label: "Availability", icon: "calendar" }, { id: "limits", label: "Limits", icon: "time" }, @@ -60,13 +117,11 @@ const tabs = [ export default function EventTypeDetail() { const router = useRouter(); - const { id, title, description, duration, price, currency, slug } = useLocalSearchParams<{ + const { id, title, description, duration, slug } = useLocalSearchParams<{ id: string; title: string; description?: string; duration: string; - price?: string; - currency?: string; slug?: string; }>(); @@ -81,9 +136,12 @@ export default function EventTypeDetail() { const [username, setUsername] = useState("username"); const [allowMultipleDurations, setAllowMultipleDurations] = useState(false); const [locations, setLocations] = useState([]); - const [locationAddress, setLocationAddress] = useState(""); - const [locationLink, setLocationLink] = useState(""); - const [locationPhone, setLocationPhone] = useState(""); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_locationAddress, setLocationAddress] = useState(""); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_locationLink, setLocationLink] = useState(""); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_locationPhone, setLocationPhone] = useState(""); const [selectedDurations, setSelectedDurations] = useState([]); const [defaultDuration, setDefaultDuration] = useState(""); const [showDurationDropdown, setShowDurationDropdown] = useState(false); @@ -128,8 +186,6 @@ export default function EventTypeDetail() { const [rollingCalendarDays, setRollingCalendarDays] = useState(false); const [rangeStartDate, setRangeStartDate] = useState(""); const [rangeEndDate, setRangeEndDate] = useState(""); - const [offsetStartTimes, setOffsetStartTimes] = useState(false); - const [offsetStartValue, setOffsetStartValue] = useState("0"); // Advanced tab state const [calendarEventName, setCalendarEventName] = useState(""); @@ -243,13 +299,6 @@ export default function EventTypeDetail() { ); }; - const getSelectedLocationIconUrl = (): string | null => { - if (locations.length > 0) { - return locations[0].iconUrl; - } - return null; - }; - const toggleDurationSelection = (duration: string) => { const newSelected = selectedDurations.includes(duration) ? selectedDurations.filter((d) => d !== duration) @@ -313,7 +362,23 @@ export default function EventTypeDetail() { ); }; - const fetchSchedules = async () => { + const fetchScheduleDetails = useCallback(async (scheduleId: number) => { + try { + setScheduleDetailsLoading(true); + const scheduleDetails = await CalComAPIService.getScheduleById(scheduleId); + setSelectedScheduleDetails(scheduleDetails); + if (scheduleDetails?.timeZone) { + setSelectedTimezone(scheduleDetails.timeZone); + } + } catch (error) { + console.error("Failed to fetch schedule details:", error); + setSelectedScheduleDetails(null); + } finally { + setScheduleDetailsLoading(false); + } + }, []); + + const fetchSchedules = useCallback(async () => { try { setSchedulesLoading(true); const schedulesData = await CalComAPIService.getSchedules(); @@ -330,25 +395,9 @@ export default function EventTypeDetail() { } finally { setSchedulesLoading(false); } - }; + }, [fetchScheduleDetails]); - const fetchScheduleDetails = async (scheduleId: number) => { - try { - setScheduleDetailsLoading(true); - const scheduleDetails = await CalComAPIService.getScheduleById(scheduleId); - setSelectedScheduleDetails(scheduleDetails); - if (scheduleDetails?.timeZone) { - setSelectedTimezone(scheduleDetails.timeZone); - } - } catch (error) { - console.error("Failed to fetch schedule details:", error); - setSelectedScheduleDetails(null); - } finally { - setScheduleDetailsLoading(false); - } - }; - - const fetchConferencingOptions = async () => { + const fetchConferencingOptions = useCallback(async () => { try { setConferencingLoading(true); const options = await CalComAPIService.getConferencingOptions(); @@ -358,13 +407,13 @@ export default function EventTypeDetail() { } finally { setConferencingLoading(false); } - }; + }, []); - const fetchEventTypeData = async () => { + const fetchEventTypeData = useCallback(async () => { if (!id) return; try { - const eventType = await CalComAPIService.getEventTypeById(parseInt(id)); + const eventType = await CalComAPIService.getEventTypeById(parseInt(id, 10)); if (eventType) { setEventTypeData(eventType); @@ -375,14 +424,14 @@ export default function EventTypeDetail() { if (eventType.lengthInMinutes) setEventDuration(eventType.lengthInMinutes.toString()); if (eventType.hidden !== undefined) setIsHidden(eventType.hidden); - const eventTypeAny = eventType as any; + const eventTypeExt = eventType as EventType & EventTypeExtended; if ( - eventTypeAny.lengthInMinutesOptions && - Array.isArray(eventTypeAny.lengthInMinutesOptions) && - eventTypeAny.lengthInMinutesOptions.length > 0 + eventTypeExt.lengthInMinutesOptions && + Array.isArray(eventTypeExt.lengthInMinutesOptions) && + eventTypeExt.lengthInMinutesOptions.length > 0 ) { setAllowMultipleDurations(true); - const durationStrings = eventTypeAny.lengthInMinutesOptions.map( + const durationStrings = eventTypeExt.lengthInMinutesOptions.map( (mins: number) => `${mins} mins` ); setSelectedDurations(durationStrings); @@ -425,7 +474,7 @@ export default function EventTypeDetail() { // Load booking frequency limits if (eventType.bookingLimitsCount && !("disabled" in eventType.bookingLimitsCount)) { setLimitBookingFrequency(true); - const limits: Array<{ id: number; value: string; unit: string }> = []; + const limits: { id: number; value: string; unit: string }[] = []; let idCounter = 1; if (eventType.bookingLimitsCount.day) { limits.push({ @@ -463,7 +512,7 @@ export default function EventTypeDetail() { // Load duration limits if (eventType.bookingLimitsDuration && !("disabled" in eventType.bookingLimitsDuration)) { setLimitTotalDuration(true); - const limits: Array<{ id: number; value: string; unit: string }> = []; + const limits: { id: number; value: string; unit: string }[] = []; let idCounter = 1; if (eventType.bookingLimitsDuration.day) { limits.push({ @@ -504,7 +553,8 @@ export default function EventTypeDetail() { } if (eventType.bookerActiveBookingsLimit) { - const bookingLimit = eventType.bookerActiveBookingsLimit as any; + const bookingLimit = + eventType.bookerActiveBookingsLimit as BookerActiveBookingsLimitExtended; if (!("disabled" in bookingLimit)) { const maxBookings = bookingLimit.maximumActiveBookings ?? bookingLimit.count; if (maxBookings !== undefined) { @@ -537,28 +587,26 @@ export default function EventTypeDetail() { } } - const eventTypeAnyForAdvanced = eventType as any; - - if (eventTypeAnyForAdvanced.disableCancelling !== undefined) { - setDisableCancelling(eventTypeAnyForAdvanced.disableCancelling); + if (eventTypeExt.disableCancelling !== undefined) { + setDisableCancelling(eventTypeExt.disableCancelling); } else if (eventType.metadata?.disableCancelling) { setDisableCancelling(true); } - if (eventTypeAnyForAdvanced.disableRescheduling !== undefined) { - setDisableRescheduling(eventTypeAnyForAdvanced.disableRescheduling); + if (eventTypeExt.disableRescheduling !== undefined) { + setDisableRescheduling(eventTypeExt.disableRescheduling); } else if (eventType.metadata?.disableRescheduling) { setDisableRescheduling(true); } - if (eventTypeAnyForAdvanced.sendCalVideoTranscription !== undefined) { - setSendCalVideoTranscription(eventTypeAnyForAdvanced.sendCalVideoTranscription); + if (eventTypeExt.sendCalVideoTranscription !== undefined) { + setSendCalVideoTranscription(eventTypeExt.sendCalVideoTranscription); } else if (eventType.metadata?.sendCalVideoTranscription) { setSendCalVideoTranscription(true); } - if (eventTypeAnyForAdvanced.autoTranslate !== undefined) { - setAutoTranslate(eventTypeAnyForAdvanced.autoTranslate); + if (eventTypeExt.autoTranslate !== undefined) { + setAutoTranslate(eventTypeExt.autoTranslate); } else if (eventType.metadata?.autoTranslate) { setAutoTranslate(true); } @@ -588,7 +636,7 @@ export default function EventTypeDetail() { } if (eventType.confirmationPolicy) { - const policy = eventType.confirmationPolicy as any; + const policy = eventType.confirmationPolicy as ConfirmationPolicyExtended; if (!("disabled" in policy) || policy.disabled === false) { setRequiresConfirmation(true); } @@ -606,14 +654,14 @@ export default function EventTypeDetail() { if (eventType.lockTimeZoneToggleOnBookingPage !== undefined) { setLockTimezone(eventType.lockTimeZoneToggleOnBookingPage); } - if (eventTypeAny.lockedTimeZone) { - setLockedTimezone(eventTypeAny.lockedTimeZone); + if (eventTypeExt.lockedTimeZone) { + setLockedTimezone(eventTypeExt.lockedTimeZone); } - if (eventTypeAny.hideCalendarEventDetails !== undefined) { - setHideCalendarEventDetails(eventTypeAny.hideCalendarEventDetails); + if (eventTypeExt.hideCalendarEventDetails !== undefined) { + setHideCalendarEventDetails(eventTypeExt.hideCalendarEventDetails); } - if (eventTypeAny.hideOrganizerEmail !== undefined) { - setHideOrganizerEmail(eventTypeAny.hideOrganizerEmail); + if (eventTypeExt.hideOrganizerEmail !== undefined) { + setHideOrganizerEmail(eventTypeExt.hideOrganizerEmail); } // Load redirect URL @@ -624,12 +672,12 @@ export default function EventTypeDetail() { setForwardParamsSuccessRedirect(eventType.forwardParamsSuccessRedirect); } - if (eventTypeAny.color) { - if (eventTypeAny.color.lightThemeHex) { - setEventTypeColorLight(eventTypeAny.color.lightThemeHex); + if (eventTypeExt.color) { + if (eventTypeExt.color.lightThemeHex) { + setEventTypeColorLight(eventTypeExt.color.lightThemeHex); } - if (eventTypeAny.color.darkThemeHex) { - setEventTypeColorDark(eventTypeAny.color.darkThemeHex); + if (eventTypeExt.color.darkThemeHex) { + setEventTypeColorDark(eventTypeExt.color.darkThemeHex); } } if (eventType.eventTypeColor) { @@ -642,7 +690,7 @@ export default function EventTypeDetail() { } if (eventType.recurrence) { - const recurrence = eventType.recurrence as any; + const recurrence = eventType.recurrence as RecurrenceExtended; if (recurrence.disabled !== true && recurrence.interval && recurrence.frequency) { setRecurringEnabled(true); setRecurringInterval(recurrence.interval.toString()); @@ -655,7 +703,9 @@ export default function EventTypeDetail() { } if (eventType.locations && eventType.locations.length > 0) { - const mappedLocations = eventType.locations.map((loc: any) => mapApiLocationToItem(loc)); + const mappedLocations = eventType.locations.map((loc: ApiLocation) => + mapApiLocationToItem(loc) + ); setLocations(mappedLocations); const firstLocation = eventType.locations[0]; @@ -675,7 +725,7 @@ export default function EventTypeDetail() { } if (eventType.seats) { - const seats = eventType.seats as any; + const seats = eventType.seats as SeatsExtended; const seatsAreEnabled = seats.disabled === false || (!("disabled" in seats) && seats.seatsPerTimeSlot); @@ -696,7 +746,7 @@ export default function EventTypeDetail() { } catch (error) { console.error("Failed to fetch event type data:", error); } - }; + }, [id]); useEffect(() => { if (activeTab === "availability") { @@ -705,13 +755,13 @@ export default function EventTypeDetail() { if (activeTab === "basics") { fetchConferencingOptions(); } - }, [activeTab]); + }, [activeTab, fetchConferencingOptions, fetchSchedules]); useEffect(() => { // Fetch event type data and conferencing options on initial load fetchEventTypeData(); fetchConferencingOptions(); - }, [id]); + }, [fetchConferencingOptions, fetchEventTypeData]); useEffect(() => { const fetchUsername = async () => { @@ -745,7 +795,7 @@ export default function EventTypeDetail() { minute: "2-digit", hour12: true, }); - } catch (error) { + } catch { return time; // Return original if parsing fails } }; @@ -829,9 +879,9 @@ export default function EventTypeDetail() { style: "destructive", onPress: async () => { try { - const eventTypeId = parseInt(id); + const eventTypeId = parseInt(id, 10); - if (isNaN(eventTypeId)) { + if (Number.isNaN(eventTypeId)) { throw new Error("Invalid event type ID"); } @@ -865,8 +915,8 @@ export default function EventTypeDetail() { return; } - const durationNum = parseInt(eventDuration); - if (isNaN(durationNum) || durationNum <= 0) { + const durationNum = parseInt(eventDuration, 10); + if (Number.isNaN(durationNum) || durationNum <= 0) { Alert.alert("Error", "Duration must be a positive number"); return; } @@ -890,7 +940,7 @@ export default function EventTypeDetail() { if (isCreateMode) { // For CREATE mode, build full payload - const payload: any = { + const payload: CreateEventTypePayload = { title: eventTitle, slug: eventSlug, lengthInMinutes: durationNum, @@ -1003,7 +1053,7 @@ export default function EventTypeDetail() { return; } - await CalComAPIService.updateEventType(parseInt(id), payload); + await CalComAPIService.updateEventType(parseInt(id, 10), payload); Alert.alert("Success", "Event type updated successfully"); // Refresh event type data to sync with server await fetchEventTypeData(); @@ -1095,7 +1145,7 @@ export default function EventTypeDetail() { > @@ -1141,7 +1191,7 @@ export default function EventTypeDetail() { > @@ -1989,3 +2039,4 @@ export default function EventTypeDetail() { ); } +// test unused variable diff --git a/companion/app/oauth/callback.tsx b/companion/app/oauth/callback.tsx index 7fe90790a6..bfcee43771 100644 --- a/companion/app/oauth/callback.tsx +++ b/companion/app/oauth/callback.tsx @@ -1,6 +1,6 @@ -import React, { useEffect } from "react"; -import { View, Text, ActivityIndicator, Platform } from "react-native"; -import { useRouter, useLocalSearchParams } from "expo-router"; +import { useLocalSearchParams, useRouter } from "expo-router"; +import { useEffect } from "react"; +import { ActivityIndicator, Platform, Text, View } from "react-native"; import { useAuth } from "../../contexts"; export default function OAuthCallback() { @@ -93,7 +93,8 @@ export default function OAuthCallback() { } } } - }, [params, router]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [params, router, auth.userInfo]); return ( diff --git a/companion/babel.config.js b/companion/babel.config.js index 18767383ab..2526f2b0df 100644 --- a/companion/babel.config.js +++ b/companion/babel.config.js @@ -1,4 +1,4 @@ -module.exports = function (api) { +module.exports = (api) => { api.cache(true); return { presets: [["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel"], diff --git a/companion/biome.json b/companion/biome.json new file mode 100644 index 0000000000..98cc685dec --- /dev/null +++ b/companion/biome.json @@ -0,0 +1,82 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": true, + "includes": [ + "**/*.js", + "**/*.jsx", + "**/*.ts", + "**/*.tsx", + "**/*.json", + "!**/node_modules", + "!**/.expo", + "!**/dist", + "!**/.output", + "!**/*.d.ts", + "!**/bun.lock" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 100 + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "error", + "noArrayIndexKey": "error", + "useIterableCallbackReturn": "error", + "noImplicitAnyLet": "off" + }, + "correctness": { + "useExhaustiveDependencies": "error", + "useHookAtTopLevel": "error", + "noInvalidUseBeforeDeclaration": "off", + "noUnusedFunctionParameters": "error", + "noUnusedVariables": "error" + }, + "complexity": { + "noStaticOnlyClass": "error" + }, + "style": { + "noNonNullAssertion": "error" + }, + "a11y": { + "noStaticElementInteractions": "error" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "always" + } + }, + "assist": { + "enabled": true, + "actions": { + "source": { + "organizeImports": "on" + } + } + }, + "overrides": [ + { + "includes": ["extension/**/*.ts", "extension/**/*.tsx"], + "javascript": { + "globals": ["chrome", "browser"] + } + } + ] +} diff --git a/companion/bun.lock b/companion/bun.lock index d801e147ca..0c78cd9433 100644 --- a/companion/bun.lock +++ b/companion/bun.lock @@ -12,7 +12,7 @@ "@react-native-segmented-control/segmented-control": "^2.5.7", "@tanstack/react-query": "^5.62.0", "@tanstack/react-query-persist-client": "^5.62.0", - "base64-js": "^1.5.1", + "base64-js": "1.5.1", "expo": "^55.0.0-canary-20251216-3f01dbf", "expo-auth-session": "7.0.11-canary-20251216-3f01dbf", "expo-clipboard": "8.0.9-canary-20251216-3f01dbf", @@ -40,14 +40,13 @@ "react-native-worklets": "0.7.1", }, "devDependencies": { + "@biomejs/biome": "2.3.10", "@types/chrome": "^0.1.31", "@types/react": "19.2.3", "@types/react-dom": "19.2.3", "babel-preset-expo": "^54.0.7", "husky": "^9.0.11", "lint-staged": "^15.2.0", - "prettier": "^3.2.5", - "prettier-plugin-tailwindcss": "^0.5.11", "tailwindcss": "^3.4.17", "typescript": "^5.9.3", "wxt": "^0.20.11", @@ -251,6 +250,24 @@ "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], + "@biomejs/biome": ["@biomejs/biome@2.3.10", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.10", "@biomejs/cli-darwin-x64": "2.3.10", "@biomejs/cli-linux-arm64": "2.3.10", "@biomejs/cli-linux-arm64-musl": "2.3.10", "@biomejs/cli-linux-x64": "2.3.10", "@biomejs/cli-linux-x64-musl": "2.3.10", "@biomejs/cli-win32-arm64": "2.3.10", "@biomejs/cli-win32-x64": "2.3.10" }, "bin": { "biome": "bin/biome" } }, "sha512-/uWSUd1MHX2fjqNLHNL6zLYWBbrJeG412/8H7ESuK8ewoRoMPUgHDebqKrPTx/5n6f17Xzqc9hdg3MEqA5hXnQ=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-M6xUjtCVnNGFfK7HMNKa593nb7fwNm43fq1Mt71kpLpb+4mE7odO8W/oWVDyBVO4ackhresy1ZYO7OJcVo/B7w=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.3.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-Vae7+V6t/Avr8tVbFNjnFSTKZogZHFYl7MMH62P/J1kZtr0tyRQ9Fe0onjqjS2Ek9lmNLmZc/VR5uSekh+p1fg=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-hhPw2V3/EpHKsileVOFynuWiKRgFEV48cLe0eA+G2wO4SzlwEhLEB9LhlSrVeu2mtSn205W283LkX7Fh48CaxA=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.3.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-B9DszIHkuKtOH2IFeeVkQmSMVUjss9KtHaNXquYYWCjH8IstNgXgx5B0aSBQNr6mn4RcKKRQZXn9Zu1rM3O0/A=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-wwAkWD1MR95u+J4LkWP74/vGz+tRrIQvr8kfMMJY8KOQ8+HMVleREOcPYsQX82S7uueco60L58Wc6M1I9WA9Dw=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.3.10", "", { "os": "linux", "cpu": "x64" }, "sha512-QTfHZQh62SDFdYc2nfmZFuTm5yYb4eO1zwfB+90YxUumRCR171tS1GoTX5OD0wrv4UsziMPmrePMtkTnNyYG3g=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.3.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-o7lYc9n+CfRbHvkjPhm8s9FgbKdYZu5HCcGVMItLjz93EhgJ8AM44W+QckDqLA9MKDNFrR8nPbO4b73VC5kGGQ=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.3.10", "", { "os": "win32", "cpu": "x64" }, "sha512-pHEFgq7dUEsKnqG9mx9bXihxGI49X+ar+UBrEIj3Wqj3UCZp1rNgV+OoyjFgcXsjCWpuEAF4VJdkZr3TrWdCbQ=="], + "@devicefarmer/adbkit": ["@devicefarmer/adbkit@3.3.8", "", { "dependencies": { "@devicefarmer/adbkit-logcat": "^2.1.2", "@devicefarmer/adbkit-monkey": "~1.2.1", "bluebird": "~3.7", "commander": "^9.1.0", "debug": "~4.3.1", "node-forge": "^1.3.1", "split": "~1.0.1" }, "bin": { "adbkit": "bin/adbkit" } }, "sha512-7rBLLzWQnBwutH2WZ0EWUkQdihqrnLYCUMaB44hSol9e0/cdIhuNFcqZO0xNheAU6qqHVA8sMiLofkYTgb+lmw=="], "@devicefarmer/adbkit-logcat": ["@devicefarmer/adbkit-logcat@2.1.3", "", {}, "sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw=="], @@ -1497,10 +1514,6 @@ "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - "prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="], - - "prettier-plugin-tailwindcss": ["prettier-plugin-tailwindcss@0.5.14", "", { "peerDependencies": { "@ianvs/prettier-plugin-sort-imports": "*", "@prettier/plugin-pug": "*", "@shopify/prettier-plugin-liquid": "*", "@trivago/prettier-plugin-sort-imports": "*", "@zackad/prettier-plugin-twig-melody": "*", "prettier": "^3.0", "prettier-plugin-astro": "*", "prettier-plugin-css-order": "*", "prettier-plugin-import-sort": "*", "prettier-plugin-jsdoc": "*", "prettier-plugin-marko": "*", "prettier-plugin-organize-attributes": "*", "prettier-plugin-organize-imports": "*", "prettier-plugin-sort-imports": "*", "prettier-plugin-style-order": "*", "prettier-plugin-svelte": "*" }, "optionalPeers": ["@ianvs/prettier-plugin-sort-imports", "@prettier/plugin-pug", "@shopify/prettier-plugin-liquid", "@trivago/prettier-plugin-sort-imports", "@zackad/prettier-plugin-twig-melody", "prettier-plugin-astro", "prettier-plugin-css-order", "prettier-plugin-import-sort", "prettier-plugin-jsdoc", "prettier-plugin-marko", "prettier-plugin-organize-attributes", "prettier-plugin-organize-imports", "prettier-plugin-sort-imports", "prettier-plugin-style-order", "prettier-plugin-svelte"] }, "sha512-Puaz+wPUAhFp8Lo9HuciYKM2Y2XExESjeT+9NQoVFXZsPPnc9VYss2SpxdQ6vbatmt8/4+SN0oe0I1cPDABg9Q=="], - "pretty-bytes": ["pretty-bytes@5.6.0", "", {}, "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="], "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], diff --git a/companion/components/AppPressable.tsx b/companion/components/AppPressable.tsx index 923ddcf01a..23f2be28d8 100644 --- a/companion/components/AppPressable.tsx +++ b/companion/components/AppPressable.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { TouchableOpacity } from "react-native"; import type { TouchableOpacityProps } from "react-native"; +import { TouchableOpacity } from "react-native"; type AppPressableProps = TouchableOpacityProps & { className?: string; diff --git a/companion/components/AuthTokenProvider.tsx b/companion/components/AuthTokenProvider.tsx index f224945459..4d86544e50 100644 --- a/companion/components/AuthTokenProvider.tsx +++ b/companion/components/AuthTokenProvider.tsx @@ -1,9 +1,9 @@ +import { useEffect } from "react"; import { useAuth } from "../contexts/AuthContext"; import { CalComAPIService } from "../services/calcom"; -import { useEffect } from "react"; export function AuthTokenProvider({ children }: { children: React.ReactNode }) { - const { accessToken, isAuthenticated, isWebSession } = useAuth(); + const { accessToken, isAuthenticated, isWebSession: _isWebSession } = useAuth(); useEffect(() => { if (isAuthenticated) { @@ -13,7 +13,7 @@ export function AuthTokenProvider({ children }: { children: React.ReactNode }) { } else { CalComAPIService.clearAuth(); } - }, [isAuthenticated, accessToken, isWebSession]); + }, [isAuthenticated, accessToken]); return <>{children}; } diff --git a/companion/components/BookingActionsModal.ios.tsx b/companion/components/BookingActionsModal.ios.tsx index bc95a2e712..090776091a 100644 --- a/companion/components/BookingActionsModal.ios.tsx +++ b/companion/components/BookingActionsModal.ios.tsx @@ -4,13 +4,13 @@ * iOS-specific modal for booking actions with Glass UI styling. * This provides a native iOS action sheet experience with Glass UI design. */ + +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 { FullScreenModal } from "./FullScreenModal"; -import { Ionicons } from "@expo/vector-icons"; -import React from "react"; -import { View, Text, TouchableOpacity, ScrollView } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; export interface BookingActionsModalProps { visible: boolean; diff --git a/companion/components/BookingActionsModal.tsx b/companion/components/BookingActionsModal.tsx index 9374b42358..81cf9ffc8e 100644 --- a/companion/components/BookingActionsModal.tsx +++ b/companion/components/BookingActionsModal.tsx @@ -9,13 +9,12 @@ * * Note: iOS uses BookingActionsModal.ios.tsx with native Glass UI styling. */ + +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 { FullScreenModal } from "./FullScreenModal"; -import { Ionicons } from "@expo/vector-icons"; -import React from "react"; -import { View, Text, TouchableOpacity, ScrollView } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; export interface BookingActionsModalProps { visible: boolean; @@ -126,8 +125,6 @@ export function BookingActionsModal({ onReportBooking, onCancelBooking, }: BookingActionsModalProps) { - const insets = useSafeAreaInsets(); - if (!booking) return null; const handleAction = (action: () => void) => { diff --git a/companion/components/CacheStatusIndicator.tsx b/companion/components/CacheStatusIndicator.tsx index d27b793d41..e82104d717 100644 --- a/companion/components/CacheStatusIndicator.tsx +++ b/companion/components/CacheStatusIndicator.tsx @@ -9,9 +9,8 @@ * to provide users with transparency about data freshness. */ -import React from "react"; -import { View, Text, StyleSheet } from "react-native"; import { Ionicons } from "@expo/vector-icons"; +import { StyleSheet, Text, View } from "react-native"; import { useQueryContext } from "../contexts/QueryContext"; /** diff --git a/companion/components/CalComLogo.tsx b/companion/components/CalComLogo.tsx index d6df1b423d..2856f174e0 100644 --- a/companion/components/CalComLogo.tsx +++ b/companion/components/CalComLogo.tsx @@ -1,4 +1,3 @@ -import React from "react"; import Svg, { Path } from "react-native-svg"; interface CalComLogoProps { diff --git a/companion/components/EmptyScreen.tsx b/companion/components/EmptyScreen.tsx index 31f3e81481..96aaa02d32 100644 --- a/companion/components/EmptyScreen.tsx +++ b/companion/components/EmptyScreen.tsx @@ -1,6 +1,5 @@ import { Ionicons } from "@expo/vector-icons"; -import React from "react"; -import { View, Text, TouchableOpacity } from "react-native"; +import { Text, TouchableOpacity, View } from "react-native"; type IoniconName = keyof typeof Ionicons.glyphMap; diff --git a/companion/components/FullScreenModal.tsx b/companion/components/FullScreenModal.tsx index bb4ce8281e..b84d31cbfc 100644 --- a/companion/components/FullScreenModal.tsx +++ b/companion/components/FullScreenModal.tsx @@ -1,5 +1,6 @@ -import React, { useEffect } from "react"; -import { Modal, ModalProps, Platform, View } from "react-native"; +import type React from "react"; +import { useEffect } from "react"; +import { Modal, type ModalProps, Platform } from "react-native"; interface FullScreenModalProps extends ModalProps { visible: boolean; diff --git a/companion/components/GlassModalHeader.tsx b/companion/components/GlassModalHeader.tsx index d88e976e1e..a6ecd9ce63 100644 --- a/companion/components/GlassModalHeader.tsx +++ b/companion/components/GlassModalHeader.tsx @@ -1,8 +1,7 @@ -import { AppPressable } from "./AppPressable"; import { Ionicons } from "@expo/vector-icons"; -import React from "react"; -import { View, Text, ActivityIndicator } from "react-native"; +import { ActivityIndicator, Text, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { AppPressable } from "./AppPressable"; interface GlassModalHeaderProps { title: string; diff --git a/companion/components/Header.tsx b/companion/components/Header.tsx index 18be182093..38aaa0db6b 100644 --- a/companion/components/Header.tsx +++ b/companion/components/Header.tsx @@ -1,50 +1,48 @@ -import { CalComAPIService, UserProfile } from "../services/calcom"; +import { Ionicons } from "@expo/vector-icons"; +import * as Clipboard from "expo-clipboard"; +import { useCallback, useEffect, useState } from "react"; +import { + ActionSheetIOS, + ActivityIndicator, + Alert, + Image, + Platform, + Text, + TouchableOpacity, + 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 { CalComLogo } from "./CalComLogo"; import { FullScreenModal } from "./FullScreenModal"; -import { Ionicons } from "@expo/vector-icons"; -import * as Clipboard from "expo-clipboard"; -import { useRouter } from "expo-router"; -import React, { useState, useEffect } from "react"; -import { - View, - Text, - TouchableOpacity, - Image, - ActivityIndicator, - Platform, - ActionSheetIOS, - Alert, -} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; export function Header() { - const router = useRouter(); const insets = useSafeAreaInsets(); const [userProfile, setUserProfile] = useState(null); const [loading, setLoading] = useState(true); const [showProfileModal, setShowProfileModal] = useState(false); - useEffect(() => { - fetchUserProfile(); - }, []); - - const fetchUserProfile = async () => { + const fetchUserProfile = useCallback(async () => { try { const profile = await CalComAPIService.getUserProfile(); setUserProfile(profile); - } catch (error) { + } catch (err) { console.error("Failed to fetch user profile"); if (__DEV__) { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; console.debug("[Header] fetchUserProfile failed", { message, stack }); } } finally { setLoading(false); } - }; + }, []); + + useEffect(() => { + fetchUserProfile(); + }, [fetchUserProfile]); // Build public page URL const publicPageUrl = userProfile?.username ? `https://cal.com/${userProfile.username}` : null; @@ -60,11 +58,11 @@ export function Header() { try { await Clipboard.setStringAsync(publicPageUrl); Alert.alert("Link Copied!", "Your public page link has been copied to clipboard."); - } catch (error) { + } catch (err) { console.error("Failed to copy public page link"); if (__DEV__) { - const message = error instanceof Error ? error.message : String(error); - const stack = error instanceof Error ? error.stack : undefined; + const message = err instanceof Error ? err.message : String(err); + const stack = err instanceof Error ? err.stack : undefined; console.debug("[Header] copyPublicPageLink failed", { message, stack }); } Alert.alert("Error", "Failed to copy link. Please try again."); diff --git a/companion/components/LoadingSpinner.tsx b/companion/components/LoadingSpinner.tsx index 44f080c169..83feb670fd 100644 --- a/companion/components/LoadingSpinner.tsx +++ b/companion/components/LoadingSpinner.tsx @@ -1,6 +1,5 @@ -import React from "react"; -import { View, ActivityIndicator, Platform, StyleSheet } from "react-native"; import { GlassView, isLiquidGlassAvailable } from "expo-glass-effect"; +import { ActivityIndicator, Platform, StyleSheet, View } from "react-native"; interface LoadingSpinnerProps { size?: "small" | "large"; diff --git a/companion/components/LocationsList.tsx b/companion/components/LocationsList.tsx index eac87c36c3..b5a83b96f6 100644 --- a/companion/components/LocationsList.tsx +++ b/companion/components/LocationsList.tsx @@ -3,27 +3,27 @@ * Reusable component for displaying and managing multiple event type locations */ -import React, { useState } from "react"; -import { - View, - Text, - TouchableOpacity, - TextInput, - Modal, - ScrollView, - Platform, - ActionSheetIOS, -} from "react-native"; import { Ionicons } from "@expo/vector-icons"; - -import { SvgImage } from "./SvgImage"; -import { LocationItem, LocationOptionGroup } from "../types/locations"; +import type React from "react"; +import { useState } from "react"; +import { + ActionSheetIOS, + Modal, + Platform, + ScrollView, + Text, + TextInput, + TouchableOpacity, + View, +} from "react-native"; +import type { LocationItem, LocationOptionGroup } from "../types/locations"; import { - locationRequiresInput, - getLocationInputPlaceholder, - getLocationInputLabel, createLocationItemFromOption, + getLocationInputLabel, + getLocationInputPlaceholder, + locationRequiresInput, } from "../utils/locationHelpers"; +import { SvgImage } from "./SvgImage"; interface LocationsListProps { /** Array of current locations */ @@ -67,7 +67,7 @@ export const LocationsList: React.FC = ({ const handleAddLocation = () => { if (Platform.OS === "ios") { - const allOptions: Array<{ label: string; value: string }> = []; + const allOptions: { label: string; value: string }[] = []; locationOptions.forEach((group) => { group.options.forEach((option) => { if (!isLocationAlreadyAdded(locations, option.value)) { @@ -161,7 +161,7 @@ export const LocationsList: React.FC = ({ {/* Locations List */} {locations.length > 0 ? ( - {locations.map((location, index) => ( + {locations.map((location, _index) => ( diff --git a/companion/components/LoginScreen.tsx b/companion/components/LoginScreen.tsx index e4ea04f3e3..205570ce60 100644 --- a/companion/components/LoginScreen.tsx +++ b/companion/components/LoginScreen.tsx @@ -1,10 +1,9 @@ +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 { CalComLogo } from "./CalComLogo"; -import React from "react"; -import { View, Text, TouchableOpacity, Platform, StyleSheet } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; export function LoginScreen() { const { loginWithOAuth, loading } = useAuth(); diff --git a/companion/components/LogoutButton.tsx b/companion/components/LogoutButton.tsx index 1b98820050..c74587417e 100644 --- a/companion/components/LogoutButton.tsx +++ b/companion/components/LogoutButton.tsx @@ -1,7 +1,7 @@ +import { useState } from "react"; +import { Alert, Platform, Text, TouchableOpacity } from "react-native"; import { useAuth } from "../contexts/AuthContext"; import { LogoutConfirmModal } from "./LogoutConfirmModal"; -import React, { useState } from "react"; -import { TouchableOpacity, Text, Alert, Platform } from "react-native"; interface LogoutButtonProps { className?: string; diff --git a/companion/components/LogoutConfirmModal.tsx b/companion/components/LogoutConfirmModal.tsx index fc83ca65bb..a50db634bb 100644 --- a/companion/components/LogoutConfirmModal.tsx +++ b/companion/components/LogoutConfirmModal.tsx @@ -1,5 +1,4 @@ -import React from "react"; -import { View, Text, TouchableOpacity, Platform } from "react-native"; +import { Platform, Text, TouchableOpacity, View } from "react-native"; import { FullScreenModal } from "./FullScreenModal"; interface LogoutConfirmModalProps { diff --git a/companion/components/NetworkStatusBanner.tsx b/companion/components/NetworkStatusBanner.tsx index a750baae2a..ff2a8275da 100644 --- a/companion/components/NetworkStatusBanner.tsx +++ b/companion/components/NetworkStatusBanner.tsx @@ -7,10 +7,10 @@ * - Shows again on next disconnect */ -import React, { useEffect, useState, useRef } from "react"; -import { View, Text, TouchableOpacity, Modal, Animated } from "react-native"; import { Ionicons } from "@expo/vector-icons"; -import NetInfo, { NetInfoState } from "@react-native-community/netinfo"; +import NetInfo, { type NetInfoState } from "@react-native-community/netinfo"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { Animated, Modal, Text, TouchableOpacity, View } from "react-native"; export function NetworkStatusBanner() { const [showModal, setShowModal] = useState(false); @@ -20,11 +20,11 @@ export function NetworkStatusBanner() { const previousOfflineRef = useRef(null); const userDismissedRef = useRef(false); - const checkIfOffline = (state: NetInfoState): boolean => { + const checkIfOffline = useCallback((state: NetInfoState): boolean => { if (state.isConnected === false) return true; if (state.isInternetReachable === false) return true; return false; - }; + }, []); useEffect(() => { const handleNetworkChange = (state: NetInfoState) => { @@ -57,7 +57,7 @@ export function NetworkStatusBanner() { // Listen for changes const unsubscribe = NetInfo.addEventListener(handleNetworkChange); return () => unsubscribe(); - }, []); + }, [checkIfOffline]); useEffect(() => { if (showModal) { diff --git a/companion/components/SvgImage.tsx b/companion/components/SvgImage.tsx index 511ff09e9d..dd939ff92a 100644 --- a/companion/components/SvgImage.tsx +++ b/companion/components/SvgImage.tsx @@ -1,5 +1,6 @@ -import React, { useState } from "react"; -import { View, Image } from "react-native"; +import type React from "react"; +import { useState } from "react"; +import { Image, View } from "react-native"; import { SvgUri } from "react-native-svg"; interface SvgImageProps { diff --git a/companion/components/Tooltip.web.tsx b/companion/components/Tooltip.web.tsx index dbed2544a0..996b01e8bd 100644 --- a/companion/components/Tooltip.web.tsx +++ b/companion/components/Tooltip.web.tsx @@ -1,4 +1,5 @@ -import React, { useState } from "react"; +import type React from "react"; +import { useState } from "react"; interface TooltipProps { text: string; @@ -17,6 +18,7 @@ export function Tooltip({ text, children }: TooltipProps) { }; return ( + // biome-ignore lint/a11y/noStaticElementInteractions: Tooltip wrapper only shows/hides content on hover, not a clickable element { const scheduleActions: { label: string; - icon: any; + icon: SFSymbols7_0; onPress: () => void; role: "default" | "destructive"; }[] = [ @@ -22,7 +23,7 @@ export const AvailabilityListItem = ({ ? [ { label: "Set as Default", - icon: "star", + icon: "star" as const, onPress: () => onSetAsDefault(schedule), role: "default" as const, }, @@ -30,15 +31,15 @@ export const AvailabilityListItem = ({ : []), { label: "Duplicate", - icon: "square.on.square", + icon: "square.on.square" as const, onPress: () => onDuplicate?.(schedule), - role: "default", + role: "default" as const, }, { label: "Delete", - icon: "trash", + icon: "trash" as const, onPress: () => onDelete?.(schedule), - role: "destructive", + role: "destructive" as const, }, ]; diff --git a/companion/components/availability-list-item/AvailabilityListItem.tsx b/companion/components/availability-list-item/AvailabilityListItem.tsx index 3c587a4f73..8c18763f72 100644 --- a/companion/components/availability-list-item/AvailabilityListItem.tsx +++ b/companion/components/availability-list-item/AvailabilityListItem.tsx @@ -1,6 +1,6 @@ import { Ionicons } from "@expo/vector-icons"; import { Text, TouchableOpacity, View } from "react-native"; -import { Schedule } from "../../hooks"; +import type { Schedule } from "../../hooks"; export interface AvailabilityListItemProps { item: Schedule; diff --git a/companion/components/booking-list-item/BookingListItem.ios.tsx b/companion/components/booking-list-item/BookingListItem.ios.tsx index a5d89e5f81..9428582bee 100644 --- a/companion/components/booking-list-item/BookingListItem.ios.tsx +++ b/companion/components/booking-list-item/BookingListItem.ios.tsx @@ -1,16 +1,16 @@ -import { Ionicons } from "@expo/vector-icons"; -import React from "react"; -import { View, Text, TouchableOpacity, Linking, Pressable, Alert } from "react-native"; -import { Host, ContextMenu, Button, Image, HStack } from "@expo/ui/swift-ui"; +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 { BookingListItemProps } from "./types"; -import { SvgImage } from "../SvgImage"; -import { getMeetingInfo } from "../../utils/meetings-utils"; -import { formatTime, formatDate, getHostAndAttendeesDisplay } from "../../utils/bookings-utils"; +import React from "react"; +import { Linking, Pressable, Text, TouchableOpacity, 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 type { BookingListItemProps } from "./types"; export const BookingListItem: React.FC = ({ booking, @@ -21,7 +21,7 @@ export const BookingListItem: React.FC = ({ onLongPress, onConfirm, onReject, - onActionsPress, + onActionsPress: _onActionsPress, onReschedule, onEditLocation, onAddGuests, @@ -37,14 +37,12 @@ export const BookingListItem: React.FC = ({ const isPending = booking.status?.toUpperCase() === "PENDING"; const isCancelled = booking.status?.toUpperCase() === "CANCELLED"; const isRejected = booking.status?.toUpperCase() === "REJECTED"; - const hasLocationUrl = !!booking.location; - 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: any) => att.noShow === true || att.absent === true + (att: { noShow?: boolean; absent?: boolean }) => att.noShow === true || att.absent === true ); // Use centralized action gating for consistency @@ -61,7 +59,7 @@ export const BookingListItem: React.FC = ({ // Define context menu actions based on booking state type ContextMenuAction = { label: string; - icon: string; + icon: SFSymbols7_0; onPress: () => void; role: "default" | "destructive"; }; @@ -70,39 +68,39 @@ export const BookingListItem: React.FC = ({ // Edit Event Section { label: "Reschedule Booking", - icon: "calendar", + icon: "calendar" as const, onPress: () => onReschedule?.(booking), - role: "default", + role: "default" as const, visible: isUpcoming && !isCancelled && !isPending && !!onReschedule, }, { label: "Edit Location", - icon: "location", + icon: "location" as const, onPress: () => onEditLocation?.(booking), - role: "default", + role: "default" as const, visible: isUpcoming && !isCancelled && !isPending && !!onEditLocation, }, { label: "Add Guests", - icon: "person.badge.plus", + icon: "person.badge.plus" as const, onPress: () => onAddGuests?.(booking), - role: "default", + role: "default" as const, visible: isUpcoming && !isCancelled && !isPending && !!onAddGuests, }, // After Event Section { label: "View Recordings", - icon: "video", + icon: "video" as const, onPress: () => onViewRecordings?.(booking), - role: "default", + role: "default" as const, visible: actions.viewRecordings.visible && actions.viewRecordings.enabled && !!onViewRecordings, }, { label: "Meeting Session Details", - icon: "info.circle", + icon: "info.circle" as const, onPress: () => onMeetingSessionDetails?.(booking), - role: "default", + role: "default" as const, visible: actions.meetingSessionDetails.visible && actions.meetingSessionDetails.enabled && @@ -110,24 +108,24 @@ export const BookingListItem: React.FC = ({ }, { label: "Mark as No-Show", - icon: "eye.slash", + icon: "eye.slash" as const, onPress: () => onMarkNoShow?.(booking), - role: "default", + role: "default" as const, visible: actions.markNoShow.visible && actions.markNoShow.enabled && !!onMarkNoShow, }, // Other Actions { label: "Report Booking", - icon: "flag", + icon: "flag" as const, onPress: () => onReportBooking?.(booking), - role: "destructive", + role: "destructive" as const, visible: !!onReportBooking, }, { label: "Cancel Event", - icon: "xmark.circle", + icon: "xmark.circle" as const, onPress: () => onCancelBooking?.(booking), - role: "destructive", + role: "destructive" as const, visible: isUpcoming && !isCancelled && !!onCancelBooking, }, ]; @@ -171,7 +169,7 @@ export const BookingListItem: React.FC = ({ {/* Description */} {booking.description ? ( - "{booking.description}" + "{booking.description}" ) : null} {/* Host and Attendees */} @@ -265,7 +263,7 @@ export const BookingListItem: React.FC = ({ {contextMenuActions.map((action) => (