fix(companion): improve code clarity and type safety in components (#26062)

- Remove 'as any' type assertion in LoginScreen.tsx, use Platform.OS check
- Fix malformed className string in LogoutButton.tsx (extra quotes)
- Remove @ts-ignore comments in Tooltip.tsx with proper web-specific types
- Replace 'any' type with Schedule type in AvailabilityTab.tsx
- Replace 'any' type with 'string' for SF Symbol icons in EventTypeListItem.ios.tsx
- Remove console.log error handling in SvgImage.tsx
- Extract duplicated formatDuration function to use shared utility from formatters.ts
- Extract repeated inline styles to sectionDividerStyle constant in AvailabilityTab.tsx

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: Dhairyashil <dhairyashil10101010@gmail.com>
This commit is contained in:
Peer Richelsen
2025-12-23 00:26:16 +00:00
committed by GitHub
co-authored by Devin AI Dhairyashil
parent 6650fdb9ef
commit 9c680838b6
8 changed files with 100 additions and 137 deletions
+5 -5
View File
@@ -1,10 +1,10 @@
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";
import { useAuth } from "../contexts/AuthContext";
import { CalComLogo } from "./CalComLogo";
import { showErrorAlert } from "../utils/alerts";
import { openInAppBrowser } from "../utils/browser";
export function LoginScreen() {
const { loginWithOAuth, loading } = useAuth();
@@ -66,7 +66,7 @@ export function LoginScreen() {
<TouchableOpacity
onPress={handleSignUp}
className="mt-3 items-center justify-center py-1"
style={{ cursor: "pointer" } as any}
style={Platform.OS === "web" ? { cursor: "pointer" } : undefined}
activeOpacity={0.7}
>
<View>
+3 -3
View File
@@ -1,7 +1,7 @@
import React, { useState } from "react";
import { TouchableOpacity, Text, Alert, Platform } 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;
@@ -62,7 +62,7 @@ export function LogoutButton({ className = "" }: LogoutButtonProps) {
<>
<TouchableOpacity
onPress={handleLogoutPress}
className={`"px-4 rounded-lg" bg-gray-600 py-2 ${className}`}
className={`rounded-lg bg-gray-600 px-4 py-2 ${className}`}
style={Platform.OS === "web" ? { cursor: "pointer" } : undefined}
activeOpacity={0.7}
>
+5 -3
View File
@@ -28,7 +28,9 @@ export const SvgImage: React.FC<SvgImageProps> = ({ uri, width, height, style })
source={{ uri }}
style={{ width, height }}
resizeMode="contain"
onError={() => console.log("Image load error:", uri)}
onError={() => {
// Image load errors are expected for invalid/missing URLs - silently handled
}}
/>
</View>
);
@@ -41,8 +43,8 @@ export const SvgImage: React.FC<SvgImageProps> = ({ uri, width, height, style })
uri={uri}
width={width}
height={height}
onError={(error) => {
console.log("SVG load error, falling back to Image:", error);
onError={() => {
// SVG load errors trigger fallback to regular Image component
setUseFallback(true);
}}
/>
+3 -62
View File
@@ -1,69 +1,10 @@
import React, { useState, useRef, useEffect } from "react";
import { View, Text, Platform } from "react-native";
interface TooltipProps {
text: string;
children: React.ReactElement;
}
export function Tooltip({ text, children }: TooltipProps) {
const [showTooltip, setShowTooltip] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const containerRef = useRef<any>(null);
if (Platform.OS !== "web") {
return children;
}
const handleMouseEnter = (e: any) => {
if (containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setPosition({
top: rect.top - 40,
left: rect.left + rect.width / 2 + 20,
});
setShowTooltip(true);
}
};
const handleMouseLeave = () => {
setShowTooltip(false);
};
return (
<View
ref={containerRef}
// @ts-ignore - web-only props
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ position: "relative" }}
>
{children}
{showTooltip ? (
<View
// @ts-ignore - web-only styles
style={{
position: "fixed",
top: position.top,
left: position.left,
transform: "translate(-50%, -100%)",
backgroundColor: "#1a1a1a",
paddingLeft: 10,
paddingRight: 10,
paddingTop: 6,
paddingBottom: 6,
borderRadius: 6,
zIndex: 10002,
pointerEvents: "none",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
}}
>
{/* @ts-ignore - web-only styles */}
<Text style={{ color: "white", fontSize: 13, fontWeight: "600", whiteSpace: "nowrap" }}>
{text}
</Text>
</View>
) : null}
</View>
);
// Native Tooltip is a no-op; web implementation lives in Tooltip.web.tsx
void text;
return children;
}
+50
View File
@@ -0,0 +1,50 @@
import React, { useState } from "react";
interface TooltipProps {
text: string;
children: React.ReactElement;
}
export function Tooltip({ text, children }: TooltipProps) {
const [showTooltip, setShowTooltip] = useState(false);
const handleMouseEnter = () => {
setShowTooltip(true);
};
const handleMouseLeave = () => {
setShowTooltip(false);
};
return (
<span
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{ position: "relative", display: "inline-flex" }}
>
{children}
{showTooltip ? (
<span
style={{
position: "absolute",
left: "50%",
bottom: "100%",
transform: "translate(-50%, -8px)",
backgroundColor: "#1a1a1a",
padding: "6px 10px",
borderRadius: 6,
zIndex: 10002,
pointerEvents: "none",
boxShadow: "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)",
color: "white",
fontSize: 13,
fontWeight: 600,
whiteSpace: "nowrap",
}}
>
{text}
</span>
) : null}
</span>
);
}
@@ -1,7 +1,7 @@
import { Schedule } from "../../../services/calcom";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { Schedule } from "../../../services/calcom";
interface DaySchedule {
day: string;
@@ -15,12 +15,21 @@ interface AvailabilityTabProps {
setShowScheduleDropdown: (show: boolean) => void;
schedulesLoading: boolean;
scheduleDetailsLoading: boolean;
selectedScheduleDetails: any;
selectedScheduleDetails: Schedule | null;
getDaySchedules: () => DaySchedule[];
formatTime: (time: string) => string;
selectedTimezone: string;
}
const sectionDividerStyle = {
borderTopWidth: 1,
borderTopColor: "#E5E5EA",
marginLeft: -20,
marginRight: -20,
paddingLeft: 20,
paddingRight: 20,
};
export function AvailabilityTab(props: AvailabilityTabProps) {
return (
<View className="rounded-2xl bg-white p-5">
@@ -44,17 +53,7 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
{props.selectedSchedule ? (
<>
<View
className="mt-5 pt-5"
style={{
borderTopWidth: 1,
borderTopColor: "#E5E5EA",
marginLeft: -20,
marginRight: -20,
paddingLeft: 20,
paddingRight: 20,
}}
>
<View className="mt-5 pt-5" style={sectionDividerStyle}>
{props.scheduleDetailsLoading ? (
<View className="items-center py-4">
<Text className="text-sm italic text-[#8E8E93]">Loading schedule details...</Text>
@@ -71,7 +70,9 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
</Text>
<Text className="mr-4 text-right text-[15px] text-[#666]">
{dayInfo.available && dayInfo.startTime && dayInfo.endTime
? `${props.formatTime(dayInfo.startTime)} - ${props.formatTime(dayInfo.endTime)}`
? `${props.formatTime(dayInfo.startTime)} - ${props.formatTime(
dayInfo.endTime
)}`
: "Unavailable"}
</Text>
</View>
@@ -85,17 +86,7 @@ export function AvailabilityTab(props: AvailabilityTabProps) {
)}
</View>
<View
className="mt-5 pt-5"
style={{
borderTopWidth: 1,
borderTopColor: "#E5E5EA",
marginLeft: -20,
marginRight: -20,
paddingLeft: 20,
paddingRight: 20,
}}
>
<View className="mt-5 pt-5" style={sectionDividerStyle}>
<Text className="mb-1.5 text-base font-semibold text-[#333]">Timezone</Text>
<View className="items-center rounded-lg bg-[#F8F9FA] px-3 py-3">
<Text className="text-center text-base text-[#666]">
@@ -1,13 +1,13 @@
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, Pressable } from "react-native";
import { Host, ContextMenu, Button, Image, HStack } from "@expo/ui/swift-ui";
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
import { EventTypeListItemProps } from "./types";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import { formatDuration } from "../../utils/formatters";
import { getEventDuration } from "../../utils/getEventDuration";
import { normalizeMarkdown } from "../../utils/normalizeMarkdown";
import { EventTypeListItemProps } from "./types";
import { Host, ContextMenu, Button, Image, HStack } from "@expo/ui/swift-ui";
import { buttonStyle, frame, padding } from "@expo/ui/swift-ui/modifiers";
import { Ionicons } from "@expo/vector-icons";
import { isLiquidGlassAvailable } from "expo-glass-effect";
import React from "react";
import { View, Text, Pressable } from "react-native";
export const EventTypeListItem = ({
item,
@@ -25,9 +25,12 @@ export const EventTypeListItem = ({
const duration = getEventDuration(item);
const isLast = index === filteredEventTypes.length - 1;
type ButtonSystemImage = React.ComponentProps<typeof Button>["systemImage"];
type EventTypeIcon = Exclude<ButtonSystemImage, undefined>;
const eventTypes: {
label: string;
icon: any;
icon: EventTypeIcon;
onPress: () => void;
role: "default" | "destructive";
}[] = [
@@ -63,18 +66,6 @@ export const EventTypeListItem = ({
},
];
const formatDuration = (minutes: number | undefined) => {
if (!minutes || minutes <= 0) {
return "0m";
}
if (minutes < 60) {
return `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
};
return (
<View className={`bg-white active:bg-[#F8F9FA] ${!isLast ? "border-b border-[#E5E5EA]" : ""}`}>
<View className="flex-shrink-1 flex-row items-center justify-between">
@@ -1,13 +1,13 @@
import { Tooltip } from "../../components/Tooltip";
import { formatDuration } from "../../utils/formatters";
import { getEventDuration } from "../../utils/getEventDuration";
import { normalizeMarkdown } from "../../utils/normalizeMarkdown";
import { EventTypeListItemProps } from "./types";
import { Ionicons } from "@expo/vector-icons";
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import Svg, { Path } from "react-native-svg";
import { Tooltip } from "../../components/Tooltip";
import { EventTypeListItemProps } from "./types";
import { getEventDuration } from "../../utils/getEventDuration";
import { normalizeMarkdown } from "../../utils/normalizeMarkdown";
export const EventTypeListItem = ({
item,
index,
@@ -21,18 +21,6 @@ export const EventTypeListItem = ({
const duration = getEventDuration(item);
const isLast = index === filteredEventTypes.length - 1;
const formatDuration = (minutes: number | undefined) => {
if (!minutes || minutes <= 0) {
return "0m";
}
if (minutes < 60) {
return `${minutes}m`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
};
return (
<TouchableOpacity
className={`bg-white active:bg-[#F8F9FA] ${!isLast ? "border-b border-[#E5E5EA]" : ""}`}