feat(companion): Event types screen update (#25847)
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { Stack } from "expo-router";
|
||||
|
||||
export default function EventTypesLayout() {
|
||||
return (
|
||||
<Stack>
|
||||
<Stack.Screen name="event-types" options={{}} />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
+115
-193
@@ -1,10 +1,9 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useRouter } from "expo-router";
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { Stack, useRouter } from "expo-router";
|
||||
import React, { useState, useMemo, Activity } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
RefreshControl,
|
||||
@@ -13,30 +12,28 @@ import {
|
||||
Share,
|
||||
Alert,
|
||||
Platform,
|
||||
Modal,
|
||||
KeyboardAvoidingView,
|
||||
} from "react-native";
|
||||
import * as Clipboard from "expo-clipboard";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
|
||||
import { CalComAPIService, EventType } from "../../services/calcom";
|
||||
import { Header } from "../../components/Header";
|
||||
import { Tooltip } from "../../components/Tooltip";
|
||||
import { FullScreenModal } from "../../components/FullScreenModal";
|
||||
import { LoadingSpinner } from "../../components/LoadingSpinner";
|
||||
import { EmptyScreen } from "../../components/EmptyScreen";
|
||||
import { slugify } from "../../utils/slugify";
|
||||
import { showErrorAlert } from "../../utils/alerts";
|
||||
import { offlineAwareRefresh } from "../../utils/network";
|
||||
import { openInAppBrowser } from "../../utils/browser";
|
||||
import { formatDuration } from "../../components/event-type-detail/utils";
|
||||
import { CalComAPIService, EventType } from "../../../services/calcom";
|
||||
import { Header } from "../../../components/Header";
|
||||
import { FullScreenModal } from "../../../components/FullScreenModal";
|
||||
import { LoadingSpinner } from "../../../components/LoadingSpinner";
|
||||
import { EmptyScreen } from "../../../components/EmptyScreen";
|
||||
import { slugify } from "../../../utils/slugify";
|
||||
import { showErrorAlert } from "../../../utils/alerts";
|
||||
import { EventTypeListItem } from "../../../components/event-type-list-item/EventTypeListItem";
|
||||
import { offlineAwareRefresh } from "../../../utils/network";
|
||||
import { openInAppBrowser } from "../../../utils/browser";
|
||||
import {
|
||||
useEventTypes,
|
||||
useCreateEventType,
|
||||
useDeleteEventType,
|
||||
useDuplicateEventType,
|
||||
useUsername,
|
||||
} from "../../hooks";
|
||||
} from "../../../hooks";
|
||||
import { getEventDuration } from "../../../utils/getEventDuration";
|
||||
import { normalizeMarkdown } from "../../../utils/normalizeMarkdown";
|
||||
|
||||
export default function EventTypes() {
|
||||
const router = useRouter();
|
||||
@@ -126,49 +123,6 @@ export default function EventTypes() {
|
||||
setSearchQuery(query);
|
||||
};
|
||||
|
||||
const getDuration = (eventType: EventType): number => {
|
||||
// Prefer lengthInMinutes (API field), fallback to length for backwards compatibility
|
||||
return eventType.lengthInMinutes ?? eventType.length ?? 0;
|
||||
};
|
||||
|
||||
const normalizeMarkdown = (text: string): string => {
|
||||
if (!text) return "";
|
||||
|
||||
return (
|
||||
text
|
||||
// Remove HTML tags including <br>, <div>, <p>, etc.
|
||||
.replace(/<[^>]*>/g, " ")
|
||||
// Convert HTML entities
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/ /g, " ")
|
||||
// Convert markdown links [text](url) to just text
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
// Remove bold/italic markers **text** or *text*
|
||||
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
||||
.replace(/\*([^*]+)\*/g, "$1")
|
||||
// Remove inline code `text`
|
||||
.replace(/`([^`]+)`/g, "$1")
|
||||
// Remove strikethrough ~~text~~
|
||||
.replace(/~~([^~]+)~~/g, "$1")
|
||||
// Remove heading markers # ## ###
|
||||
.replace(/^#{1,6}\s+/gm, "")
|
||||
// Remove blockquote markers >
|
||||
.replace(/^>\s+/gm, "")
|
||||
// Remove list markers - * +
|
||||
.replace(/^[\s]*[-*+]\s+/gm, "")
|
||||
// Remove numbered list markers 1. 2. etc
|
||||
.replace(/^[\s]*\d+\.\s+/gm, "")
|
||||
// Normalize multiple whitespace/newlines to single space
|
||||
.replace(/\s+/g, " ")
|
||||
// Trim whitespace
|
||||
.trim()
|
||||
);
|
||||
};
|
||||
|
||||
const handleEventTypePress = (eventType: EventType) => {
|
||||
handleEdit(eventType);
|
||||
};
|
||||
@@ -251,7 +205,7 @@ export default function EventTypes() {
|
||||
};
|
||||
|
||||
const handleEdit = (eventType: EventType) => {
|
||||
const duration = getDuration(eventType);
|
||||
const duration = getEventDuration(eventType);
|
||||
router.push({
|
||||
pathname: "/event-type-detail",
|
||||
params: {
|
||||
@@ -308,7 +262,7 @@ export default function EventTypes() {
|
||||
Alert.alert("Success", "Event type duplicated successfully");
|
||||
}
|
||||
|
||||
const duration = getDuration(eventType);
|
||||
const duration = getEventDuration(eventType);
|
||||
|
||||
// Navigate to edit the newly created duplicate
|
||||
router.push({
|
||||
@@ -424,103 +378,12 @@ export default function EventTypes() {
|
||||
);
|
||||
};
|
||||
|
||||
const renderEventType = ({ item, index }: { item: EventType; index: number }) => {
|
||||
const duration = getDuration(item);
|
||||
const isLast = index === filteredEventTypes.length - 1;
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
className={`bg-white active:bg-[#F8F9FA] ${!isLast ? "border-b border-[#E5E5EA]" : ""}`}
|
||||
onPress={() => handleEventTypePress(item)}
|
||||
onLongPress={() => handleEventTypeLongPress(item)}
|
||||
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="mr-4 flex-1">
|
||||
<View className="mb-1 flex-row items-center">
|
||||
<Text className="flex-1 text-base font-semibold text-[#333]">{item.title}</Text>
|
||||
</View>
|
||||
{item.description && (
|
||||
<Text className="mb-2 mt-0.5 text-sm leading-5 text-[#666]" numberOfLines={2}>
|
||||
{normalizeMarkdown(item.description)}
|
||||
</Text>
|
||||
)}
|
||||
<View className="mt-2 flex-row items-center self-start rounded-lg border border-[#E5E5EA] bg-[#E5E5EA] px-2 py-1">
|
||||
<Ionicons name="time-outline" size={14} color="#000" />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-black">
|
||||
{formatDuration(duration)}
|
||||
</Text>
|
||||
</View>
|
||||
{(item.price != null && item.price > 0) || item.requiresConfirmation ? (
|
||||
<View className="mt-2 flex-row items-center gap-3">
|
||||
{item.price != null && item.price > 0 && (
|
||||
<Text className="text-sm font-medium text-[#34C759]">
|
||||
{item.currency || "$"}
|
||||
{item.price}
|
||||
</Text>
|
||||
)}
|
||||
{item.requiresConfirmation && (
|
||||
<View className="rounded bg-[#FF9500] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">Requires Confirmation</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<View className="flex-row">
|
||||
<Tooltip text="Preview">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-l-lg border border-r-0 border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handlePreview(item)}
|
||||
>
|
||||
<Ionicons name="open-outline" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<Tooltip text={copiedEventTypeId === item.id ? "Copied!" : "Copy link"}>
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center border border-r-0 border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleCopyLink(item)}
|
||||
>
|
||||
{copiedEventTypeId === item.id ? (
|
||||
<Ionicons name="checkmark" size={18} color="#10B981" />
|
||||
) : (
|
||||
<Svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#3C3F44"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<Path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<Path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</Svg>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<Tooltip text="More">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-r-lg border border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleEventTypeLongPress(item)}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<Header />
|
||||
<Activity mode={Platform.OS === "web" ? "visible" : "hidden"}>
|
||||
<Header />
|
||||
</Activity>
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<LoadingSpinner size="large" />
|
||||
</View>
|
||||
@@ -531,7 +394,9 @@ export default function EventTypes() {
|
||||
if (error) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<Header />
|
||||
<Activity mode={Platform.OS === "web" ? "visible" : "hidden"}>
|
||||
<Header />
|
||||
</Activity>
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<Ionicons name="alert-circle" size={64} color="#FF3B30" />
|
||||
<Text className="mb-2 mt-4 text-center text-xl font-bold text-gray-800">
|
||||
@@ -549,7 +414,9 @@ export default function EventTypes() {
|
||||
if (eventTypes.length === 0) {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<Header />
|
||||
<Activity mode={Platform.OS === "web" ? "visible" : "hidden"}>
|
||||
<Header />
|
||||
</Activity>
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<EmptyScreen
|
||||
icon="link-outline"
|
||||
@@ -566,6 +433,77 @@ export default function EventTypes() {
|
||||
if (filteredEventTypes.length === 0 && searchQuery.trim() !== "") {
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<Activity mode={Platform.OS === "web" ? "visible" : "hidden"}>
|
||||
<Header />
|
||||
<View className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2">
|
||||
<TextInput
|
||||
className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-[17px] text-black focus:border-black focus:ring-2 focus:ring-black"
|
||||
placeholder="Search event types"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
clearButtonMode="while-editing"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
|
||||
onPress={handleCreateNew}
|
||||
>
|
||||
<Ionicons name="add" size={18} color="#fff" />
|
||||
<Text className="text-base font-semibold text-white">New</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Activity>
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<EmptyScreen
|
||||
icon="search-outline"
|
||||
headline={`No results found for "${searchQuery}"`}
|
||||
description="Try searching with different keywords"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "Event Types",
|
||||
headerLargeTitleEnabled: true,
|
||||
headerStyle: {
|
||||
backgroundColor: "transparent",
|
||||
},
|
||||
headerSearchBarOptions: {
|
||||
placeholder: "Search event types",
|
||||
barTintColor: "#fff",
|
||||
obscureBackground: false,
|
||||
onChangeText: (e) => {
|
||||
handleSearch(e.nativeEvent.text);
|
||||
},
|
||||
},
|
||||
unstable_headerRightItems: () => [
|
||||
{
|
||||
type: "button",
|
||||
label: "New",
|
||||
labelStyle: {
|
||||
// style if needed
|
||||
},
|
||||
variant: "prominent",
|
||||
tintColor: "#000",
|
||||
// icon: {
|
||||
// name: "plus",
|
||||
// type: "sfSymbol",
|
||||
// },
|
||||
onPress: handleCreateNew,
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
|
||||
<Activity mode={Platform.OS === "web" ? "visible" : "hidden"}>
|
||||
<Header />
|
||||
<View className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2">
|
||||
<TextInput
|
||||
@@ -586,48 +524,32 @@ export default function EventTypes() {
|
||||
<Text className="text-base font-semibold text-white">New</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View className="flex-1 items-center justify-center bg-gray-50 p-5">
|
||||
<EmptyScreen
|
||||
icon="search-outline"
|
||||
headline={`No results found for "${searchQuery}"`}
|
||||
description="Try searching with different keywords"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
</Activity>
|
||||
|
||||
return (
|
||||
<View className="flex-1 bg-gray-100">
|
||||
<Header />
|
||||
<View className="flex-row items-center gap-3 border-b border-gray-300 bg-gray-100 px-4 py-2">
|
||||
<TextInput
|
||||
className="flex-1 rounded-lg border border-gray-200 bg-white px-3 py-2 text-[17px] text-black focus:border-black focus:ring-2 focus:ring-black"
|
||||
placeholder="Search event types"
|
||||
placeholderTextColor="#9CA3AF"
|
||||
value={searchQuery}
|
||||
onChangeText={handleSearch}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
clearButtonMode="while-editing"
|
||||
/>
|
||||
<TouchableOpacity
|
||||
className="min-w-[60px] flex-row items-center justify-center gap-1 rounded-lg bg-black px-2.5 py-2"
|
||||
onPress={handleCreateNew}
|
||||
>
|
||||
<Ionicons name="add" size={18} color="#fff" />
|
||||
<Text className="text-base font-semibold text-white">New</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<ScrollView
|
||||
style={{ backgroundColor: "white" }}
|
||||
contentContainerStyle={{ paddingBottom: 90 }}
|
||||
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentInsetAdjustmentBehavior="automatic"
|
||||
>
|
||||
<View className="px-2 pt-4 md:px-4">
|
||||
<View className="overflow-hidden rounded-lg border border-[#E5E5EA] bg-white">
|
||||
{filteredEventTypes.map((item, index) => (
|
||||
<View key={item.id.toString()}>{renderEventType({ item, index })}</View>
|
||||
<EventTypeListItem
|
||||
key={item.id.toString()}
|
||||
item={item}
|
||||
index={index}
|
||||
filteredEventTypes={filteredEventTypes}
|
||||
copiedEventTypeId={copiedEventTypeId}
|
||||
handleEventTypePress={handleEventTypePress}
|
||||
handleEventTypeLongPress={handleEventTypeLongPress}
|
||||
handleCopyLink={handleCopyLink}
|
||||
handlePreview={handlePreview}
|
||||
onEdit={handleEdit}
|
||||
onDuplicate={handleDuplicate}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
@@ -988,6 +910,6 @@ export default function EventTypes() {
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ export default function TabLayout() {
|
||||
if (supportsLiquidGlass) {
|
||||
return (
|
||||
<NativeTabs>
|
||||
<NativeTabs.Trigger name="event-types">
|
||||
<NativeTabs.Trigger name="(event-types)">
|
||||
<NativeTabs.Trigger.Icon sf="link" />
|
||||
<NativeTabs.Trigger.Label>Event Types</NativeTabs.Trigger.Label>
|
||||
</NativeTabs.Trigger>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Redirect } from "expo-router";
|
||||
|
||||
export default function Index() {
|
||||
return <Redirect href="/(tabs)/event-types" />;
|
||||
}
|
||||
+23
-2
@@ -16,9 +16,10 @@
|
||||
"base64-js": "^1.5.1",
|
||||
"expo": "^55.0.0-canary-20251211-7da85ea",
|
||||
"expo-auth-session": "7.0.11-canary-20251211-7da85ea",
|
||||
"expo-clipboard": "~8.0.8",
|
||||
"expo-clipboard": "8.0.9-canary-20251211-7da85ea",
|
||||
"expo-constants": "18.1.0-canary-20251211-7da85ea",
|
||||
"expo-crypto": "15.0.9-canary-20251211-7da85ea",
|
||||
"expo-dev-client": "6.1.0-canary-20251211-7da85ea",
|
||||
"expo-device": "8.0.11-canary-20251211-7da85ea",
|
||||
"expo-glass-effect": "0.2.0-canary-20251211-7da85ea",
|
||||
"expo-linking": "8.0.11-canary-20251211-7da85ea",
|
||||
@@ -633,6 +634,8 @@
|
||||
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||
|
||||
"anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="],
|
||||
|
||||
"ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="],
|
||||
@@ -933,12 +936,20 @@
|
||||
|
||||
"expo-auth-session": ["expo-auth-session@7.0.11-canary-20251211-7da85ea", "", { "dependencies": { "expo-application": "7.0.9-canary-20251211-7da85ea", "expo-constants": "18.1.0-canary-20251211-7da85ea", "expo-crypto": "15.0.9-canary-20251211-7da85ea", "expo-linking": "8.0.11-canary-20251211-7da85ea", "expo-web-browser": "15.0.11-canary-20251211-7da85ea", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-1LGdSc0Ekl4Z/BRepAD4qMxE1DsWdg/XW2KQXo43C2hjFYkrQS6hJFMufBrggbpjqAmqlODKORyoU6iPe1b2nQ=="],
|
||||
|
||||
"expo-clipboard": ["expo-clipboard@8.0.8", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-VKoBkHIpZZDJTB0jRO4/PZskHdMNOEz3P/41tmM6fDuODMpqhvyWK053X0ebspkxiawJX9lX33JXHBCvVsTTOA=="],
|
||||
"expo-clipboard": ["expo-clipboard@8.0.9-canary-20251211-7da85ea", "", { "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea", "react": "*", "react-native": "*" } }, "sha512-AboouYUoShq4RpVueMZtnV5wXK4JIA8DbBhqIrZ8bAxIsVPu++oVjvSZWTbMNz3fkgBENJ0iUuHV2w2Tg7YfZQ=="],
|
||||
|
||||
"expo-constants": ["expo-constants@18.1.0-canary-20251211-7da85ea", "", { "dependencies": { "@expo/config": "12.0.13-canary-20251211-7da85ea", "@expo/env": "2.0.9-canary-20251211-7da85ea" }, "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea", "react-native": "*" } }, "sha512-9K9+qBGaB9mQ8ae349s8+r20/DKvLPDs39LwMVhQcjhn5v9U+Pq8wvxEjPSMHoX7CnoG6juEC/nieXyw3fJ8xQ=="],
|
||||
|
||||
"expo-crypto": ["expo-crypto@15.0.9-canary-20251211-7da85ea", "", { "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea" } }, "sha512-+5AlurZ6lpgTgVy1bUF/BuuLhIDXfx0+dmXyZ2arKdI6UWzsQ0+iLiqfxRlMYFNjLYXRbH4KqU8BUYWLjt3Icg=="],
|
||||
|
||||
"expo-dev-client": ["expo-dev-client@6.1.0-canary-20251211-7da85ea", "", { "dependencies": { "expo-dev-launcher": "6.1.0-canary-20251211-7da85ea", "expo-dev-menu": "7.1.0-canary-20251211-7da85ea", "expo-dev-menu-interface": "2.0.1-canary-20251211-7da85ea", "expo-manifests": "1.0.11-canary-20251211-7da85ea", "expo-updates-interface": "2.0.1-canary-20251211-7da85ea" }, "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea" } }, "sha512-uKmsN1pR1yEqfxnRWl4hVI6QgwhYFD9UTPEFPz63b9z6jdfIcRvs1MrIeezOO3eTWkBLXi10DeFW1cmnfYcerQ=="],
|
||||
|
||||
"expo-dev-launcher": ["expo-dev-launcher@6.1.0-canary-20251211-7da85ea", "", { "dependencies": { "ajv": "^8.11.0", "expo-dev-menu": "7.1.0-canary-20251211-7da85ea", "expo-manifests": "1.0.11-canary-20251211-7da85ea" }, "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea" } }, "sha512-ECb7DC8lqmLka/ZMzjvNoI9USVvTiMCuWK3l5auUjsB/zleKLw3mNv62XMk326Lwvd68MUvbSipWGhkbVPGKkg=="],
|
||||
|
||||
"expo-dev-menu": ["expo-dev-menu@7.1.0-canary-20251211-7da85ea", "", { "dependencies": { "expo-dev-menu-interface": "2.0.1-canary-20251211-7da85ea" }, "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea" } }, "sha512-9axnKD+8UEFaTPRYqr0mlh7RuMa8dwruLQdYJwY2s7p9eNEjcQLoACiCfOnTqEAKa6u5tHAku5z3rS/928WJUQ=="],
|
||||
|
||||
"expo-dev-menu-interface": ["expo-dev-menu-interface@2.0.1-canary-20251211-7da85ea", "", { "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea" } }, "sha512-upWPcYkXIqZe56njdLW7oTuPVHZtsYggb1xGgR07D1ioucm6l38d3qPhE9mVxuwGfmoy/tzck71DA7Txus8bmg=="],
|
||||
|
||||
"expo-device": ["expo-device@8.0.11-canary-20251211-7da85ea", "", { "dependencies": { "ua-parser-js": "^0.7.33" }, "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea" } }, "sha512-JI5UYYSN3c5YrGB42PQf9Wm/wWLpYJ9PfRLog5HwYJ7fWYLe9BOdmy56P77PsQUQ+rBkF4nQF2Qtfvms1TrMjQ=="],
|
||||
|
||||
"expo-file-system": ["expo-file-system@19.0.21-canary-20251211-7da85ea", "", { "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea", "react-native": "*" } }, "sha512-EQ5ReoU8FBoKewUHmvdmczupAp1Lk9Q9HT/PHN3Gb0edayV/RK3lavQ/EcZgtS4ksnQFxF8dsLQIWHqHi93m7Q=="],
|
||||
@@ -947,10 +958,14 @@
|
||||
|
||||
"expo-glass-effect": ["expo-glass-effect@0.2.0-canary-20251211-7da85ea", "", { "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea", "react": "*", "react-native": "*" } }, "sha512-UJaKxrkdVcUSXTU5n4L2FX8pRntaF/7p6YeP3eO69eq1Vx19bxqAhfVU5BVqfVzF61NSqR4HbbnyTRcZjCnoOg=="],
|
||||
|
||||
"expo-json-utils": ["expo-json-utils@0.15.1-canary-20251211-7da85ea", "", {}, "sha512-0sJzmPF8aa8Pa2n6PdhBZjsVbfp7eJJKRavZb7glJzN33gHI9/7v2dGi0nN6jtgMNkiqhrwCnus6mL73Gcn1kg=="],
|
||||
|
||||
"expo-keep-awake": ["expo-keep-awake@15.0.9-canary-20251211-7da85ea", "", { "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea", "react": "*" } }, "sha512-dTi8y5YWhGBBNOovKKfOfdEH5N2S36kS4EPy/flwXkr2v+yEm3e0FqIoV/yqh7VuzdngPfiIyoUbL7AVoI94nQ=="],
|
||||
|
||||
"expo-linking": ["expo-linking@8.0.11-canary-20251211-7da85ea", "", { "dependencies": { "expo-constants": "18.1.0-canary-20251211-7da85ea", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-E/m3I6936rnuw7epd5xWFklcwReaG06qMbEMI0Nb5suOQg1+DmAGpxxouH3RiJ/0Fp6NgzPbqe9YDVz6giLt9Q=="],
|
||||
|
||||
"expo-manifests": ["expo-manifests@1.0.11-canary-20251211-7da85ea", "", { "dependencies": { "@expo/config": "12.0.13-canary-20251211-7da85ea", "expo-json-utils": "0.15.1-canary-20251211-7da85ea" }, "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea" } }, "sha512-9rgYSPiKXoS1kVVqFLGF10Um8TMEldkIE9CZDStUizR48x3nB0E7MLvBDmN7D+O3M5dRebuM0PMy9XNjU2PTbg=="],
|
||||
|
||||
"expo-modules-autolinking": ["expo-modules-autolinking@3.1.0-canary-20251211-7da85ea", "", { "dependencies": { "@expo/spawn-async": "^1.7.2", "chalk": "^4.1.0", "commander": "^7.2.0", "require-from-string": "^2.0.2", "resolve-from": "^5.0.0" }, "bin": { "expo-modules-autolinking": "bin/expo-modules-autolinking.js" } }, "sha512-ec+23BQqq+X8Nqt8iBSyxdF3dyP1SmsFQGob1NUaWXuSAf2z0QDZ8apbuwmUq7hx5+8ZSKO8G/XKCFsvYxZjhA=="],
|
||||
|
||||
"expo-modules-core": ["expo-modules-core@4.0.0-canary-20251211-7da85ea", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-XWrGIXNDyknXlbY8+YfIILeDt444lxJ4i5qVh600xtoKK5PNDBpIS2XPuGCGsm8vcMmTTvdrOt1SOIZTlE+q/g=="],
|
||||
@@ -965,6 +980,8 @@
|
||||
|
||||
"expo-symbols": ["expo-symbols@1.1.0-canary-20251211-7da85ea", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0" }, "peerDependencies": { "@expo-google-fonts/material-symbols": "^0.4.1", "expo": "55.0.0-canary-20251211-7da85ea", "expo-font": "14.1.0-canary-20251211-7da85ea", "react": "*", "react-native": "*" } }, "sha512-E1YiwLl/ZUYF4OibsJeboyOJ7c+vWmDOKodtNp4maustSfooSN2INw0iv2NSWGqIsHP7p4NTtd4Hmkv8V3VKow=="],
|
||||
|
||||
"expo-updates-interface": ["expo-updates-interface@2.0.1-canary-20251211-7da85ea", "", { "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea" } }, "sha512-apAcXjLigov0e3D/oHEnABUvd1cHD1JsujN/cLM3pcY3qlZAT3cJDSXA67YR4IiDd8MdxxXd3eGUo3w4Y4uqpQ=="],
|
||||
|
||||
"expo-web-browser": ["expo-web-browser@15.0.11-canary-20251211-7da85ea", "", { "peerDependencies": { "expo": "55.0.0-canary-20251211-7da85ea", "react-native": "*" } }, "sha512-d845VCB+u/sSo2ZQJg+CJDmmrCkHEdjBMuw2rfxTEKiYcw7WWmISJ8BRal2VyV/lDU61tJxR2lxxLNAtjHbEQA=="],
|
||||
|
||||
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
|
||||
@@ -979,6 +996,8 @@
|
||||
|
||||
"fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fastq": ["fastq@1.19.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ=="],
|
||||
|
||||
"fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="],
|
||||
@@ -1199,6 +1218,8 @@
|
||||
|
||||
"json-parse-even-better-errors": ["json-parse-even-better-errors@3.0.2", "", {}, "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||
|
||||
"jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="],
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import React from "react";
|
||||
import { View, Text, TouchableOpacity, Pressable } from "react-native";
|
||||
import Svg, { Path } from "react-native-svg";
|
||||
import { Host, ContextMenu, Button, Image, Label, HStack } from "@expo/ui/swift-ui";
|
||||
|
||||
import { Tooltip } from "../../components/Tooltip";
|
||||
import {
|
||||
background,
|
||||
buttonStyle,
|
||||
clipShape,
|
||||
fixedSize,
|
||||
foregroundStyle,
|
||||
frame,
|
||||
padding,
|
||||
shapes,
|
||||
} from "@expo/ui/swift-ui/modifiers";
|
||||
import { EventTypeListItemProps } from "./types";
|
||||
import { isLiquidGlassAvailable } from "expo-glass-effect";
|
||||
import { getEventDuration } from "../../utils/getEventDuration";
|
||||
import { normalizeMarkdown } from "../../utils/normalizeMarkdown";
|
||||
|
||||
export const EventTypeListItem = ({
|
||||
item,
|
||||
index,
|
||||
filteredEventTypes,
|
||||
copiedEventTypeId,
|
||||
handleEventTypePress,
|
||||
handleEventTypeLongPress,
|
||||
handleCopyLink,
|
||||
handlePreview,
|
||||
onEdit,
|
||||
onDuplicate,
|
||||
onDelete,
|
||||
}: EventTypeListItemProps) => {
|
||||
const duration = getEventDuration(item);
|
||||
const isLast = index === filteredEventTypes.length - 1;
|
||||
|
||||
const eventTypes: {
|
||||
label: string;
|
||||
icon: any;
|
||||
onPress: () => void;
|
||||
role: "default" | "destructive";
|
||||
}[] = [
|
||||
{
|
||||
label: "Preview",
|
||||
icon: "arrow.up.right.square",
|
||||
onPress: () => handlePreview(item),
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
label: "Copy link",
|
||||
icon: "link",
|
||||
onPress: () => handleCopyLink(item),
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
label: "Edit",
|
||||
icon: "pencil",
|
||||
onPress: () => onEdit(item),
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
label: "Duplicate",
|
||||
icon: "square.on.square",
|
||||
onPress: () => onDuplicate(item),
|
||||
role: "default",
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
icon: "trash",
|
||||
onPress: () => onDelete(item),
|
||||
role: "destructive",
|
||||
},
|
||||
];
|
||||
|
||||
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">
|
||||
<Pressable
|
||||
onPress={() => handleEventTypePress(item)}
|
||||
// No need for long press on iOS
|
||||
// onLongPress={() => handleEventTypeLongPress(item)}
|
||||
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
||||
className="flex-grow"
|
||||
>
|
||||
<View className="mr-4 flex-1">
|
||||
<View className="mb-1 flex-row items-center">
|
||||
<Text className="flex-1 text-base font-semibold text-[#333]">{item.title} </Text>
|
||||
</View>
|
||||
{item.description && (
|
||||
<Text className="mb-2 mt-0.5 text-sm leading-5 text-[#666]" numberOfLines={2}>
|
||||
{normalizeMarkdown(item.description)}
|
||||
</Text>
|
||||
)}
|
||||
<View className="mt-2 flex-row items-center self-start rounded-lg border border-[#E5E5EA] bg-[#E5E5EA] px-2 py-1">
|
||||
<Ionicons name="time-outline" size={14} color="#000" />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-black">
|
||||
{formatDuration(duration)}
|
||||
</Text>
|
||||
</View>
|
||||
{(item.price != null && item.price > 0) || item.requiresConfirmation ? (
|
||||
<View className="mt-2 flex-row items-center gap-3">
|
||||
{item.price != null && item.price > 0 && (
|
||||
<Text className="text-sm font-medium text-[#34C759]">
|
||||
{item.currency || "$"}
|
||||
{item.price}
|
||||
</Text>
|
||||
)}
|
||||
{item.requiresConfirmation && (
|
||||
<View className="rounded bg-[#FF9500] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">Requires Confirmation</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
<Host matchContents>
|
||||
<ContextMenu
|
||||
modifiers={[buttonStyle(isLiquidGlassAvailable() ? "glass" : "bordered"), padding()]}
|
||||
activationMethod="singlePress"
|
||||
>
|
||||
<ContextMenu.Items>
|
||||
{eventTypes.map((eventType) => (
|
||||
<Button
|
||||
key={eventType.label}
|
||||
systemImage={eventType.icon}
|
||||
onPress={eventType.onPress}
|
||||
role={eventType.role}
|
||||
>
|
||||
{eventType.label}
|
||||
</Button>
|
||||
))}
|
||||
</ContextMenu.Items>
|
||||
<ContextMenu.Trigger>
|
||||
{/* <Image systemName="ellipsis" modifiers={[]} /> */}
|
||||
<HStack>
|
||||
<Image
|
||||
systemName="ellipsis"
|
||||
color="primary"
|
||||
size={24}
|
||||
modifiers={[frame({ height: 24, width: 17 })]}
|
||||
/>
|
||||
</HStack>
|
||||
</ContextMenu.Trigger>
|
||||
</ContextMenu>
|
||||
</Host>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,123 @@
|
||||
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,
|
||||
filteredEventTypes,
|
||||
copiedEventTypeId,
|
||||
handleEventTypePress,
|
||||
handleEventTypeLongPress,
|
||||
handleCopyLink,
|
||||
handlePreview,
|
||||
}: EventTypeListItemProps) => {
|
||||
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]" : ""}`}
|
||||
onPress={() => handleEventTypePress(item)}
|
||||
onLongPress={() => handleEventTypeLongPress(item)}
|
||||
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
||||
>
|
||||
<View className="flex-row items-center justify-between">
|
||||
<View className="mr-4 flex-1">
|
||||
<View className="mb-1 flex-row items-center">
|
||||
<Text className="flex-1 text-base font-semibold text-[#333]">{item.title}</Text>
|
||||
</View>
|
||||
{item.description && (
|
||||
<Text className="mb-2 mt-0.5 text-sm leading-5 text-[#666]" numberOfLines={2}>
|
||||
{normalizeMarkdown(item.description)}
|
||||
</Text>
|
||||
)}
|
||||
<View className="mt-2 flex-row items-center self-start rounded-lg border border-[#E5E5EA] bg-[#E5E5EA] px-2 py-1">
|
||||
<Ionicons name="time-outline" size={14} color="#000" />
|
||||
<Text className="ml-1.5 text-xs font-semibold text-black">
|
||||
{formatDuration(duration)}
|
||||
</Text>
|
||||
</View>
|
||||
{(item.price != null && item.price > 0) || item.requiresConfirmation ? (
|
||||
<View className="mt-2 flex-row items-center gap-3">
|
||||
{item.price != null && item.price > 0 && (
|
||||
<Text className="text-sm font-medium text-[#34C759]">
|
||||
{item.currency || "$"}
|
||||
{item.price}
|
||||
</Text>
|
||||
)}
|
||||
{item.requiresConfirmation && (
|
||||
<View className="rounded bg-[#FF9500] px-2 py-0.5">
|
||||
<Text className="text-xs font-medium text-white">Requires Confirmation</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
<View className="flex-row">
|
||||
<Tooltip text="Preview">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-l-lg border border-r-0 border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handlePreview(item)}
|
||||
>
|
||||
<Ionicons name="open-outline" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<Tooltip text={copiedEventTypeId === item.id ? "Copied!" : "Copy link"}>
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center border border-r-0 border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleCopyLink(item)}
|
||||
>
|
||||
{copiedEventTypeId === item.id ? (
|
||||
<Ionicons name="checkmark" size={18} color="#10B981" />
|
||||
) : (
|
||||
<Svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#3C3F44"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<Path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<Path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</Svg>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
<Tooltip text="More">
|
||||
<TouchableOpacity
|
||||
className="items-center justify-center rounded-r-lg border border-[#E5E5EA]"
|
||||
style={{ width: 32, height: 32 }}
|
||||
onPress={() => handleEventTypeLongPress(item)}
|
||||
>
|
||||
<Ionicons name="ellipsis-horizontal" size={18} color="#3C3F44" />
|
||||
</TouchableOpacity>
|
||||
</Tooltip>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import { EventType } from "../../services/types/event-types.types";
|
||||
|
||||
export interface EventTypeListItemProps {
|
||||
item: EventType;
|
||||
index: number;
|
||||
filteredEventTypes: EventType[];
|
||||
copiedEventTypeId: number | null;
|
||||
handleEventTypePress: (item: EventType) => void;
|
||||
handleEventTypeLongPress: (item: EventType) => void;
|
||||
handleCopyLink: (item: EventType) => void;
|
||||
handlePreview: (item: EventType) => void;
|
||||
onEdit?: (item: EventType) => void;
|
||||
onDuplicate?: (item: EventType) => void;
|
||||
onDelete?: (item: EventType) => void;
|
||||
}
|
||||
@@ -31,6 +31,7 @@
|
||||
"expo-clipboard": "8.0.9-canary-20251211-7da85ea",
|
||||
"expo-constants": "18.1.0-canary-20251211-7da85ea",
|
||||
"expo-crypto": "15.0.9-canary-20251211-7da85ea",
|
||||
"expo-dev-client": "6.1.0-canary-20251211-7da85ea",
|
||||
"expo-device": "8.0.11-canary-20251211-7da85ea",
|
||||
"expo-glass-effect": "0.2.0-canary-20251211-7da85ea",
|
||||
"expo-linking": "8.0.11-canary-20251211-7da85ea",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { EventType } from "../hooks";
|
||||
|
||||
export const getEventDuration = (eventType: EventType): number => {
|
||||
// Prefer lengthInMinutes (API field), fallback to length for backwards compatibility
|
||||
return eventType.lengthInMinutes ?? eventType.length ?? 0;
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
export const normalizeMarkdown = (text: string): string => {
|
||||
if (!text) return "";
|
||||
|
||||
return (
|
||||
text
|
||||
// Remove HTML tags including <br>, <div>, <p>, etc.
|
||||
.replace(/<[^>]*>/g, " ")
|
||||
// Convert HTML entities
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/ /g, " ")
|
||||
// Convert markdown links [text](url) to just text
|
||||
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||
// Remove bold/italic markers **text** or *text*
|
||||
.replace(/\*\*([^*]+)\*\*/g, "$1")
|
||||
.replace(/\*([^*]+)\*/g, "$1")
|
||||
// Remove inline code `text`
|
||||
.replace(/`([^`]+)`/g, "$1")
|
||||
// Remove strikethrough ~~text~~
|
||||
.replace(/~~([^~]+)~~/g, "$1")
|
||||
// Remove heading markers # ## ###
|
||||
.replace(/^#{1,6}\s+/gm, "")
|
||||
// Remove blockquote markers >
|
||||
.replace(/^>\s+/gm, "")
|
||||
// Remove list markers - * +
|
||||
.replace(/^[\s]*[-*+]\s+/gm, "")
|
||||
// Remove numbered list markers 1. 2. etc
|
||||
.replace(/^[\s]*\d+\.\s+/gm, "")
|
||||
// Normalize multiple whitespace/newlines to single space
|
||||
.replace(/\s+/g, " ")
|
||||
// Trim whitespace
|
||||
.trim()
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user