* 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
334 lines
12 KiB
TypeScript
334 lines
12 KiB
TypeScript
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";
|
|
|
|
export function Header() {
|
|
const insets = useSafeAreaInsets();
|
|
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showProfileModal, setShowProfileModal] = useState(false);
|
|
|
|
const fetchUserProfile = useCallback(async () => {
|
|
try {
|
|
const profile = await CalComAPIService.getUserProfile();
|
|
setUserProfile(profile);
|
|
} catch (err) {
|
|
console.error("Failed to fetch user profile");
|
|
if (__DEV__) {
|
|
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;
|
|
|
|
const handleViewPublicPage = () => {
|
|
if (publicPageUrl) {
|
|
openInAppBrowser(publicPageUrl, "Public page");
|
|
}
|
|
};
|
|
|
|
const handleCopyPublicPageLink = async () => {
|
|
if (!publicPageUrl) return;
|
|
try {
|
|
await Clipboard.setStringAsync(publicPageUrl);
|
|
Alert.alert("Link Copied!", "Your public page link has been copied to clipboard.");
|
|
} catch (err) {
|
|
console.error("Failed to copy public page link");
|
|
if (__DEV__) {
|
|
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.");
|
|
}
|
|
};
|
|
|
|
const handleProfile = () => {
|
|
if (Platform.OS === "ios") {
|
|
const options = [
|
|
"Cancel",
|
|
"My Profile",
|
|
"My Settings",
|
|
"Out of Office",
|
|
"View public page",
|
|
"Copy public page link",
|
|
"Help",
|
|
];
|
|
|
|
ActionSheetIOS.showActionSheetWithOptions(
|
|
{
|
|
options,
|
|
cancelButtonIndex: 0,
|
|
title: userProfile?.name || "Profile Menu",
|
|
},
|
|
(buttonIndex) => {
|
|
switch (buttonIndex) {
|
|
case 1: // My Profile
|
|
handleMenuOption("profile");
|
|
break;
|
|
case 2: // My Settings
|
|
handleMenuOption("settings");
|
|
break;
|
|
case 3: // Out of Office
|
|
handleMenuOption("outOfOffice");
|
|
break;
|
|
case 4: // View public page
|
|
handleViewPublicPage();
|
|
break;
|
|
case 5: // Copy public page link
|
|
handleCopyPublicPageLink();
|
|
break;
|
|
case 6: // Help
|
|
handleMenuOption("help");
|
|
break;
|
|
}
|
|
}
|
|
);
|
|
} else {
|
|
setShowProfileModal(true);
|
|
}
|
|
};
|
|
|
|
const handleMenuOption = (option: string) => {
|
|
if (Platform.OS !== "ios") {
|
|
setShowProfileModal(false);
|
|
}
|
|
switch (option) {
|
|
case "profile":
|
|
openInAppBrowser("https://app.cal.com/settings/my-account/profile", "Profile page");
|
|
break;
|
|
case "settings":
|
|
openInAppBrowser("https://app.cal.com/settings/my-account", "Settings page");
|
|
break;
|
|
case "outOfOffice":
|
|
openInAppBrowser(
|
|
"https://app.cal.com/settings/my-account/out-of-office",
|
|
"Out of Office page"
|
|
);
|
|
break;
|
|
case "roadmap":
|
|
openInAppBrowser("https://cal.com/roadmap", "Roadmap");
|
|
break;
|
|
case "help":
|
|
openInAppBrowser("https://cal.com/help", "Help page");
|
|
break;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<View
|
|
className="flex-row items-center justify-between border-b border-[#E5E5EA] bg-white px-2 md:px-4"
|
|
style={{ paddingTop: insets.top + 4, paddingBottom: 4 }}
|
|
>
|
|
{/* Left: Cal.com Logo */}
|
|
<View className="ms-1">
|
|
<CalComLogo width={101} height={22} color="#333" />
|
|
</View>
|
|
|
|
{/* Right: Icons */}
|
|
<View
|
|
className="flex-row items-center gap-4"
|
|
style={Platform.OS === "web" ? { marginRight: 8 } : {}}
|
|
>
|
|
{/* Profile Picture */}
|
|
<TouchableOpacity onPress={handleProfile} className="p-1">
|
|
{loading ? (
|
|
<ActivityIndicator size="small" color="#666" />
|
|
) : userProfile?.avatarUrl ? (
|
|
<Image
|
|
source={{ uri: getAvatarUrl(userProfile.avatarUrl) }}
|
|
className="h-8 w-8 rounded-full"
|
|
style={{ width: 32, height: 32, borderRadius: 16 }}
|
|
/>
|
|
) : (
|
|
<View
|
|
className="items-center justify-center rounded-full bg-[#E5E5EA]"
|
|
style={{ width: 32, height: 32 }}
|
|
>
|
|
<Ionicons name="person-outline" size={20} color="#666" />
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Profile Menu Modal */}
|
|
<FullScreenModal
|
|
visible={showProfileModal}
|
|
animationType="fade"
|
|
transparent
|
|
onRequestClose={() => setShowProfileModal(false)}
|
|
>
|
|
<TouchableOpacity
|
|
className="flex-1 items-center justify-center bg-black/50 p-2 md:p-4"
|
|
activeOpacity={1}
|
|
onPress={() => setShowProfileModal(false)}
|
|
>
|
|
<TouchableOpacity
|
|
className="mx-4 w-full max-w-sm rounded-2xl bg-white"
|
|
activeOpacity={1}
|
|
onPress={(e) => e.stopPropagation()}
|
|
>
|
|
{/* Header with profile info */}
|
|
<View className="border-b border-gray-200 p-6">
|
|
<View className="flex-row items-center">
|
|
<View className="mr-3 h-12 w-12 items-center justify-center rounded-full bg-black">
|
|
{userProfile?.avatarUrl ? (
|
|
<Image
|
|
source={{ uri: getAvatarUrl(userProfile.avatarUrl) }}
|
|
className="h-12 w-12 rounded-full"
|
|
style={{ width: 48, height: 48, borderRadius: 24 }}
|
|
/>
|
|
) : (
|
|
<Text className="text-lg font-semibold text-white">
|
|
{userProfile?.name
|
|
? userProfile.name.charAt(0).toUpperCase()
|
|
: userProfile?.email?.charAt(0).toUpperCase() || "?"}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
<View className="flex-1">
|
|
<Text className="text-lg font-semibold text-gray-900">
|
|
{userProfile?.name || "User"}
|
|
</Text>
|
|
{userProfile?.email ? (
|
|
<Text className="text-sm text-gray-600">{userProfile.email}</Text>
|
|
) : null}
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Menu Items */}
|
|
<View className="p-2">
|
|
<TouchableOpacity
|
|
className="flex-row items-center justify-between p-2 hover:bg-gray-50 md:p-4"
|
|
onPress={() => handleMenuOption("profile")}
|
|
>
|
|
<View className="flex-row items-center">
|
|
<Ionicons name="person-outline" size={20} color="#6B7280" />
|
|
<Text className="ml-3 text-base text-gray-900">My Profile</Text>
|
|
</View>
|
|
<Ionicons name="open-outline" size={16} color="#6B7280" />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
className="flex-row items-center justify-between p-2 hover:bg-gray-50 md:p-4"
|
|
onPress={() => handleMenuOption("settings")}
|
|
>
|
|
<View className="flex-row items-center">
|
|
<Ionicons name="settings-outline" size={20} color="#6B7280" />
|
|
<Text className="ml-3 text-base text-gray-900">My Settings</Text>
|
|
</View>
|
|
<Ionicons name="open-outline" size={16} color="#6B7280" />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
className="flex-row items-center justify-between p-2 hover:bg-gray-50 md:p-4"
|
|
onPress={() => handleMenuOption("outOfOffice")}
|
|
>
|
|
<View className="flex-row items-center">
|
|
<Ionicons name="moon-outline" size={20} color="#6B7280" />
|
|
<Text className="ml-3 text-base text-gray-900">Out of Office</Text>
|
|
</View>
|
|
<Ionicons name="open-outline" size={16} color="#6B7280" />
|
|
</TouchableOpacity>
|
|
|
|
<View className="mx-4 my-2 h-px bg-gray-200" />
|
|
|
|
{/* View public page */}
|
|
<TouchableOpacity
|
|
className="flex-row items-center justify-between p-2 hover:bg-gray-50 md:p-4"
|
|
onPress={() => {
|
|
setShowProfileModal(false);
|
|
handleViewPublicPage();
|
|
}}
|
|
>
|
|
<View className="flex-row items-center">
|
|
<Ionicons name="globe-outline" size={20} color="#6B7280" />
|
|
<Text className="ml-3 text-base text-gray-900">View public page</Text>
|
|
</View>
|
|
<Ionicons name="open-outline" size={16} color="#6B7280" />
|
|
</TouchableOpacity>
|
|
|
|
{/* Copy public page link */}
|
|
<TouchableOpacity
|
|
className="flex-row items-center justify-between p-2 hover:bg-gray-50 md:p-4"
|
|
onPress={() => {
|
|
setShowProfileModal(false);
|
|
handleCopyPublicPageLink();
|
|
}}
|
|
>
|
|
<View className="flex-row items-center">
|
|
<Ionicons name="copy-outline" size={20} color="#6B7280" />
|
|
<Text className="ml-3 text-base text-gray-900">Copy public page link</Text>
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
<View className="mx-4 my-2 h-px bg-gray-200" />
|
|
|
|
<TouchableOpacity
|
|
className="flex-row items-center justify-between p-2 hover:bg-gray-50 md:p-4"
|
|
onPress={() => handleMenuOption("roadmap")}
|
|
>
|
|
<View className="flex-row items-center">
|
|
<Ionicons name="map-outline" size={20} color="#6B7280" />
|
|
<Text className="ml-3 text-base text-gray-900">Roadmap</Text>
|
|
</View>
|
|
<Ionicons name="open-outline" size={16} color="#6B7280" />
|
|
</TouchableOpacity>
|
|
|
|
<TouchableOpacity
|
|
className="flex-row items-center justify-between p-2 hover:bg-gray-50 md:p-4"
|
|
onPress={() => handleMenuOption("help")}
|
|
>
|
|
<View className="flex-row items-center">
|
|
<Ionicons name="help-circle-outline" size={20} color="#6B7280" />
|
|
<Text className="ml-3 text-base text-gray-900">Help</Text>
|
|
</View>
|
|
<Ionicons name="open-outline" size={16} color="#6B7280" />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
{/* Cancel button */}
|
|
<View className="border-t border-gray-200 p-2 md:p-4">
|
|
<TouchableOpacity
|
|
className="w-full rounded-lg bg-gray-100 p-3"
|
|
onPress={() => setShowProfileModal(false)}
|
|
>
|
|
<Text className="text-center text-base font-medium text-gray-700">Cancel</Text>
|
|
</TouchableOpacity>
|
|
</View>
|
|
</TouchableOpacity>
|
|
</TouchableOpacity>
|
|
</FullScreenModal>
|
|
</>
|
|
);
|
|
}
|