Files
calendar/companion/hooks/useEventTypes.ts
T
Dhairyashil ShindeandGitHub 9203cb262c 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.
2025-12-28 08:08:33 -03:00

298 lines
8.6 KiB
TypeScript

/**
* Event Types Query Hooks
*
* This module provides React Query hooks for fetching and mutating event types.
* It integrates with the existing CalComAPIService and provides:
* - Automatic caching with configurable stale times
* - Pull-to-refresh support via refetch
* - Optimistic updates for mutations
* - Cache invalidation on create/update/delete
*/
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";
/**
* Hook to fetch all event types
*
* @returns Query result with event types data, loading state, error, and refetch function
*
* @example
* ```tsx
* const { data: eventTypes, isLoading, refetch, isRefetching } = useEventTypes();
*
* // Pull-to-refresh
* <RefreshControl refreshing={isRefetching} onRefresh={refetch} />
* ```
*/
export function useEventTypes() {
return useQuery({
queryKey: queryKeys.eventTypes.lists(),
queryFn: () => CalComAPIService.getEventTypes(),
staleTime: CACHE_CONFIG.eventTypes.staleTime,
// Keep previous data while fetching new data (smoother UX)
placeholderData: (previousData) => previousData,
// Don't retry on network errors (keeps cache intact)
retry: (failureCount, error) => {
if (error?.message?.includes("Network") || error?.message?.includes("fetch")) {
return false;
}
return failureCount < 2;
},
refetchOnReconnect: true,
});
}
/**
* Hook to fetch a single event type by ID
*
* @param id - The ID of the event type
* @returns Query result with event type data
*
* @example
* ```tsx
* const { data: eventType, isLoading } = useEventTypeById(123);
* ```
*/
export function useEventTypeById(id: number | undefined) {
return useQuery({
queryKey: queryKeys.eventTypes.detail(id || 0),
queryFn: () => {
if (!id) throw new Error("id is required");
return CalComAPIService.getEventTypeById(id);
},
enabled: !!id, // Only fetch when id is provided
staleTime: CACHE_CONFIG.eventTypes.staleTime,
});
}
/**
* Hook to create a new event type
*
* @returns Mutation function and state
*
* @example
* ```tsx
* const { mutate: createEventType, isPending } = useCreateEventType();
*
* createEventType({
* title: 'Quick Chat',
* slug: 'quick-chat',
* lengthInMinutes: 15,
* });
* ```
*/
export function useCreateEventType() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (input: CreateEventTypeInput) => CalComAPIService.createEventType(input),
onSuccess: (newEventType) => {
// Invalidate the list to include the new event type
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() });
// Optionally, add the new event type to cache immediately
queryClient.setQueryData(queryKeys.eventTypes.detail(newEventType.id), newEventType);
},
onError: (error) => {
console.error("Failed to create event type");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
console.debug("[useCreateEventType] failed", { message, stack });
}
},
});
}
/**
* Hook to update an event type
*
* @returns Mutation function and state
*
* @example
* ```tsx
* const { mutate: updateEventType, isPending } = useUpdateEventType();
*
* updateEventType({
* id: 123,
* updates: { title: 'Updated Title' }
* });
* ```
*/
export function useUpdateEventType() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, updates }: { id: number; updates: Partial<CreateEventTypeInput> }) =>
CalComAPIService.updateEventType(id, updates),
onSuccess: (updatedEventType, variables) => {
// Invalidate the list
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() });
// Update the specific event type in cache
queryClient.setQueryData(queryKeys.eventTypes.detail(variables.id), updatedEventType);
},
onError: (error) => {
console.error("Failed to update event type");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
console.debug("[useUpdateEventType] failed", { message, stack });
}
},
});
}
/**
* Hook to delete an event type
*
* @returns Mutation function and state
*
* @example
* ```tsx
* const { mutate: deleteEventType, isPending } = useDeleteEventType();
*
* deleteEventType(123);
* ```
*/
export function useDeleteEventType() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: number) => CalComAPIService.deleteEventType(id),
onMutate: async (deletedId) => {
// Cancel any outgoing refetches
await queryClient.cancelQueries({ queryKey: queryKeys.eventTypes.lists() });
// Snapshot the previous value
const previousEventTypes = queryClient.getQueryData<EventType[]>(
queryKeys.eventTypes.lists()
);
// Optimistically remove from the list
if (previousEventTypes) {
queryClient.setQueryData(
queryKeys.eventTypes.lists(),
previousEventTypes.filter((et) => et.id !== deletedId)
);
}
return { previousEventTypes };
},
onError: (error, _deletedId, context) => {
// Rollback on error
if (context?.previousEventTypes) {
queryClient.setQueryData(queryKeys.eventTypes.lists(), context.previousEventTypes);
}
console.error("Failed to delete event type");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
console.debug("[useDeleteEventType] failed", { message, stack });
}
},
onSettled: () => {
// Always refetch after error or success
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() });
},
});
}
/**
* Hook to duplicate an event type
*
* @returns Mutation function and state
*
* @example
* ```tsx
* const { mutate: duplicateEventType, isPending } = useDuplicateEventType();
*
* duplicateEventType({
* eventType: existingEventType,
* existingEventTypes: allEventTypes
* });
* ```
*/
export function useDuplicateEventType() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
eventType,
existingEventTypes,
}: {
eventType: EventType;
existingEventTypes: EventType[];
}) => {
// Generate a new title and slug for the duplicate
const newTitle = `${eventType.title} (copy)`;
let newSlug = `${eventType.slug}-copy`;
// Check if slug already exists and append a number if needed
let counter = 1;
while (existingEventTypes.some((et) => et.slug === newSlug)) {
newSlug = `${eventType.slug}-copy-${counter}`;
counter++;
}
const duration = eventType.lengthInMinutes ?? eventType.length ?? 15;
return CalComAPIService.createEventType({
title: newTitle,
slug: newSlug,
lengthInMinutes: duration,
description: eventType.description || undefined,
});
},
onSuccess: () => {
// Invalidate the list to include the duplicated event type
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.lists() });
},
onError: (error) => {
console.error("Failed to duplicate event type");
if (__DEV__) {
const message = error instanceof Error ? error.message : String(error);
const stack = error instanceof Error ? error.stack : undefined;
console.debug("[useDuplicateEventType] failed", { message, stack });
}
},
});
}
/**
* Hook to prefetch event types (useful for navigation)
*
* @returns Function to prefetch event types
*/
export function usePrefetchEventTypes() {
const queryClient = useQueryClient();
return () => {
queryClient.prefetchQuery({
queryKey: queryKeys.eventTypes.lists(),
queryFn: () => CalComAPIService.getEventTypes(),
staleTime: CACHE_CONFIG.eventTypes.staleTime,
});
};
}
/**
* Hook to invalidate all event types cache
*
* @returns Function to invalidate event types cache
*/
export function useInvalidateEventTypes() {
const queryClient = useQueryClient();
return () => {
queryClient.invalidateQueries({ queryKey: queryKeys.eventTypes.all });
};
}
/**
* Type exports for consumers
*/
export type { EventType, CreateEventTypeInput };