* feat(companion): add ESLint with Prettier integration
- Add ESLint with eslint-config-expo/flat for SDK 55
- Integrate Prettier via eslint-plugin-prettier
- Configure environment-specific settings for Node.js config files and browser extension
- Add lint script to package.json
- Fix Prettier formatting issues in AvailabilityTab.tsx and types/index.ts
* fix(companion): use bun lockfile instead of npm
- Remove package-lock.json that was accidentally committed
- Update bun.lock with ESLint dependencies
* fix(companion): fix all ESLint errors (16 total)
- Fix unescaped entities in JSX (react/no-unescaped-entities):
- app/(tabs)/(event-types)/index.tsx: escape quotes in delete confirmation
- components/LoginScreen.tsx: escape apostrophe in "Don't have an account"
- components/NetworkStatusBanner.tsx: escape apostrophes in offline message
- components/booking-modals/BookingModals.tsx: escape apostrophe in rejection message
- components/event-type-detail/tabs/AdvancedTab.tsx: escape quotes and apostrophes
- components/screens/AvailabilityListScreen.tsx: escape quotes in delete confirmation
- Fix react-hooks/rules-of-hooks in RescheduleScreen.tsx:
- Move useMemo calls before conditional early return to ensure hooks
are called in the same order on every render
* fix(companion): fix all ESLint warnings (68 total)
- Fix import/no-named-as-default warnings by using named imports
- Fix @typescript-eslint/no-unused-vars warnings by removing unused imports/variables
- Fix react-hooks/exhaustive-deps warnings with eslint-disable comments
- Convert unused error variables in catch blocks to bare catch
- Remove unused imports (useRouter, Alert, useSafeAreaInsets, etc.)
- All ESLint checks now pass with 0 errors and 0 warnings
* feat(companion): add pre-commit lint check with --max-warnings=0
- Add companion lint check to root lint-staged.config.mjs
- Runs 'bun run lint -- --fix --max-warnings=0' for companion files
- Follows same pattern as apps/packages lint check
- Supports SKIP_WARNINGS=1 env var to bypass warning check if needed
- Commit will fail if any ESLint errors or warnings are present
* fix(companion): restore state variables and imports removed during ESLint fixes
- Restore locationAddress, locationLink, locationPhone state variables in event-type-detail.tsx (still used in fetchEventTypeData)
- Restore Alert import in BookingListScreen.tsx (still used for report booking)
- Add eslint-disable comments for the restored state variables since they're only used by setters
* test
* test
* fix eslint pre commit check
* fix: In React Native, HTML entities like ' are rendered literally as text, not decoded like in web browsers
* refactor(companion): remove ESLint and Prettier config in preparation for Biome
* feat(companion): add Biome for linting and formatting
- Install @biomejs/biome with exact version pinning (2.3.10)
- Add biome.json with formatting settings matching previous Prettier config
- Add lint, format, check, and check:ci scripts to package.json
- Update lint-staged to use Biome instead of Prettier
- Remove Prettier and prettier-plugin-tailwindcss dependencies
- Delete .prettierrc.js configuration file
* style(companion): apply Biome formatting and fix lint-staged config
- Update lint-staged to use 'biome format' instead of 'biome check'
(pre-commit should only format, not lint)
- Apply Biome formatting to all companion files
* feat(companion): configure Biome for strict linting with pre-commit enforcement
- Apply Biome unsafe fixes (unused imports, optional chaining, etc.)
- Configure biome.json rules to disable overly strict rules for existing code:
- noExplicitAny, noArrayIndexKey, useIterableCallbackReturn (suspicious)
- useExhaustiveDependencies, noUnusedFunctionParameters (correctness)
- noStaticOnlyClass (complexity)
- noNonNullAssertion (style)
- noStaticElementInteractions (a11y)
- Update lint-staged to use 'biome check --write --error-on-warnings'
(pre-commit now runs both formatting AND linting, fails on warnings)
* fix(companion): enable noNonNullAssertion rule and fix all violations
- Enable noNonNullAssertion rule in biome.json (set to 'error')
- Fix 7 violations across the codebase:
- hooks/useBookings.ts: Add runtime check before API call
- hooks/useEventTypes.ts: Add runtime check before API call
- hooks/useSchedules.ts: Add runtime check before API call
- extension/entrypoints/content.ts: Add null checks for cache and getAttribute
* fix(companion): enable noStaticOnlyClass and noStaticElementInteractions rules
- Enable noStaticOnlyClass rule in biome.json
- Enable noStaticElementInteractions rule in biome.json
- Convert CalComAPIService from static class to object literal pattern
- Convert WebAuthService from static class to object literal pattern
- Add role="tooltip" to Tooltip.web.tsx for accessibility
* fix(companion): resolve TypeScript type errors in useEffect dependencies and remove unused state property
* fix(companion): enable noUnusedFunctionParameters rule and fix 9 violations
* fix(companion): enable noUnusedVariables rule and fix 4 violations
* fix(companion): enable useIterableCallbackReturn rule and fix 11 violations
* fix(companion): enable useExhaustiveDependencies rule and fix 7 violations
* fix(companion): enable noArrayIndexKey rule and fix 10 violations
* fix(companion): enable noExplicitAny rule and fix all violations
- Enable noExplicitAny rule in biome.json
- Replace any types with proper specific types across all files:
- calcom.ts: Add proper type definitions for API responses and inputs
- event-types.types.ts: Add BookingLimitsCount, BookingLimitsDuration, ConfirmationPolicy types
- buildPartialUpdatePayload.ts: Fix type mismatches for booking limits functions
- booking-actions.ts: Add proper types for booking action handlers
- BookingDetailScreen.tsx, MarkNoShowScreen.tsx: Add types for attendee data
- AvailabilityListItem.ios.tsx, BookingListItem.ios.tsx: Import SFSymbols7_0 type
- extension/content.ts: Fix slot types and __cleanup invocation issues
- Remove unused imports and interfaces
* address cubics comments
* add useHookAtTopLevel rule
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
/**
|
|
* Parser for Google Calendar scheduling chips in Gmail
|
|
* Extracts time slots from the Gmail ad-hoc scheduling chip
|
|
*/
|
|
|
|
export interface GoogleTimeSlot {
|
|
date: string; // "Wed, 26 November"
|
|
startTime: string; // "3:30 pm"
|
|
endTime: string; // "4:30 pm"
|
|
durationMinutes: number; // 60
|
|
isoDate: string; // "2025-11-27"
|
|
isoTimestamp: string; // "2025-11-27T05:30:00.000Z"
|
|
googleUrl: string; // Original Google Calendar URL
|
|
}
|
|
|
|
export interface ParsedGoogleChip {
|
|
scheduleId: string;
|
|
timezone: string; // "Asia/Kolkata"
|
|
timezoneOffset: string; // "GMT+05:30"
|
|
slots: GoogleTimeSlot[];
|
|
detectedDuration: number; // 15, 30, 45, or 60
|
|
}
|
|
|
|
/**
|
|
* Parse Google Calendar scheduling chip from Gmail
|
|
*/
|
|
export function parseGoogleChip(chipElement: HTMLElement): ParsedGoogleChip | null {
|
|
try {
|
|
// 1. Get schedule ID from data attribute
|
|
const scheduleId = chipElement.getAttribute("data-ad-hoc-schedule-id");
|
|
if (!scheduleId) {
|
|
console.warn("No schedule ID found in Google chip");
|
|
return null;
|
|
}
|
|
|
|
// 2. Parse timezone - first try to get IANA timezone from data attribute
|
|
let timezone = "UTC";
|
|
let timezoneOffset = "GMT+00:00";
|
|
|
|
// Try to get IANA timezone from data-ad-hoc-v2-params
|
|
const paramsAttr = chipElement.getAttribute("data-ad-hoc-v2-params");
|
|
if (paramsAttr) {
|
|
// The timezone is at the end of the params string, like: "Asia/Kolkata"
|
|
// Try multiple patterns as Gmail might format it differently
|
|
let tzMatch = paramsAttr.match(/"([^&]+)"\]/);
|
|
|
|
// Also try without HTML entities
|
|
if (!tzMatch) {
|
|
tzMatch = paramsAttr.match(/"([^"]+)"\]/);
|
|
}
|
|
|
|
// Also try with escaped quotes
|
|
if (!tzMatch) {
|
|
tzMatch = paramsAttr.match(/\\"([^\\]+)\\"\]/);
|
|
}
|
|
|
|
if (tzMatch?.[1]) {
|
|
timezone = tzMatch[1]; // e.g., "Asia/Kolkata"
|
|
}
|
|
}
|
|
|
|
// Also get the display timezone offset from the UI text
|
|
const timezoneText = chipElement.querySelector("td")?.textContent?.trim() || "";
|
|
const timezoneMatch = timezoneText.match(/^(.+?)\s*-\s*(.+?)\s*\((.+?)\)$/);
|
|
|
|
if (timezoneMatch) {
|
|
timezoneOffset = timezoneMatch[3].trim(); // e.g., "GMT+05:30"
|
|
}
|
|
|
|
// 3. Find all time slot links
|
|
const slotLinks = Array.from(chipElement.querySelectorAll("a[href*='slotStartTime']"));
|
|
|
|
if (slotLinks.length === 0) {
|
|
console.warn("No time slot links found in Google chip");
|
|
return null;
|
|
}
|
|
|
|
const slots: GoogleTimeSlot[] = [];
|
|
let detectedDuration = 60; // Default
|
|
|
|
slotLinks.forEach((link) => {
|
|
const href = (link as HTMLAnchorElement).href;
|
|
const url = new URL(href);
|
|
|
|
// Extract parameters
|
|
const slotStartTime = url.searchParams.get("slotStartTime");
|
|
const slotDurationMinutes = url.searchParams.get("slotDurationMinutes");
|
|
|
|
if (!slotStartTime || !slotDurationMinutes) {
|
|
return;
|
|
}
|
|
|
|
const durationMinutes = parseInt(slotDurationMinutes, 10);
|
|
detectedDuration = durationMinutes; // Use the duration from slots
|
|
|
|
// Convert Unix timestamp (milliseconds) to Date
|
|
const startDate = new Date(parseInt(slotStartTime, 10));
|
|
const endDate = new Date(startDate.getTime() + durationMinutes * 60 * 1000);
|
|
|
|
// Format date and time using the chip's timezone for consistency
|
|
const dateOptions: Intl.DateTimeFormatOptions = {
|
|
weekday: "short",
|
|
day: "numeric",
|
|
month: "long",
|
|
timeZone: timezone, // Use chip's timezone
|
|
};
|
|
const timeOptions: Intl.DateTimeFormatOptions = {
|
|
hour: "numeric",
|
|
minute: "2-digit",
|
|
hour12: true,
|
|
timeZone: timezone, // Use chip's timezone
|
|
};
|
|
|
|
const date = startDate.toLocaleDateString("en-US", dateOptions);
|
|
const startTime = startDate
|
|
.toLocaleTimeString("en-US", timeOptions)
|
|
.toLowerCase()
|
|
.replace(/\s/g, " ");
|
|
const endTime = endDate
|
|
.toLocaleTimeString("en-US", timeOptions)
|
|
.toLowerCase()
|
|
.replace(/\s/g, " ");
|
|
|
|
// ISO format for Cal.com
|
|
const isoDate = startDate.toISOString().split("T")[0]; // "2025-11-27"
|
|
const isoTimestamp = startDate.toISOString(); // "2025-11-27T05:30:00.000Z"
|
|
|
|
slots.push({
|
|
date,
|
|
startTime,
|
|
endTime,
|
|
durationMinutes,
|
|
isoDate,
|
|
isoTimestamp,
|
|
googleUrl: href,
|
|
});
|
|
});
|
|
|
|
if (slots.length === 0) {
|
|
console.warn("No valid slots parsed from Google chip");
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
scheduleId,
|
|
timezone,
|
|
timezoneOffset,
|
|
slots,
|
|
detectedDuration,
|
|
};
|
|
} catch (error) {
|
|
console.error("Error parsing Google chip");
|
|
if (__DEV__) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const stack = error instanceof Error ? error.stack : undefined;
|
|
console.debug("[gmailGoogleChipParser] parse failed", { message, stack });
|
|
}
|
|
return null;
|
|
}
|
|
}
|