/** * Helper functions for handling event type locations * Provides utilities for converting between API format and UI format */ import type { ApiLocation, ApiLocationInput, LocationItem, LocationOption, LocationOptionGroup, } from "@/types/locations"; import { DefaultLocationType, defaultLocations } from "./defaultLocations"; import { formatAppIdToDisplayName } from "./formatters"; import { getAppIconUrl } from "./getAppIconUrl"; // Re-export formatAppIdToDisplayName for backward compatibility export { formatAppIdToDisplayName } from "./formatters"; /** * Generate a unique ID for a location item */ export function generateLocationId(): string { return `loc_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; } /** * Map an API location response to a UI LocationItem */ export function mapApiLocationToItem(apiLocation: ApiLocation): LocationItem { const id = generateLocationId(); // Handle integration type (conferencing apps) if (apiLocation.type === "integration" && apiLocation.integration) { const iconUrl = getAppIconUrl("", apiLocation.integration); return { id, type: "integration", integration: apiLocation.integration, displayName: formatAppIdToDisplayName(apiLocation.integration), iconUrl, public: apiLocation.public, }; } // Handle address type if (apiLocation.type === "address") { return { id, type: "address", address: apiLocation.address || "", displayName: "In Person (Organizer Address)", iconUrl: "https://app.cal.com/map-pin-dark.svg", public: apiLocation.public, }; } // Handle attendeeAddress type if (apiLocation.type === "attendeeAddress") { return { id, type: "attendeeAddress", displayName: "In Person (Attendee Address)", iconUrl: "https://app.cal.com/map-pin-dark.svg", }; } // Handle link type if (apiLocation.type === "link") { return { id, type: "link", link: apiLocation.link || "", displayName: "Link Meeting", iconUrl: "https://app.cal.com/link.svg", public: apiLocation.public, }; } // Handle phone type (organizer phone) if (apiLocation.type === "phone") { return { id, type: "phone", phone: apiLocation.phone || "", displayName: "Organizer Phone Number", iconUrl: "https://app.cal.com/phone.svg", public: apiLocation.public, }; } // Handle attendeePhone type if (apiLocation.type === "attendeePhone") { return { id, type: "attendeePhone", displayName: "Attendee Phone Number", iconUrl: "https://app.cal.com/phone.svg", }; } // Handle attendeeDefined type if (apiLocation.type === "attendeeDefined") { return { id, type: "attendeeDefined", displayName: "Custom Attendee Location", iconUrl: "https://app.cal.com/message-pin.svg", }; } // Fallback for unknown types return { id, type: apiLocation.type as LocationItem["type"], displayName: apiLocation.type, iconUrl: null, }; } /** * Map a UI LocationItem to API location format for saving */ export function mapItemToApiLocation(item: LocationItem): ApiLocationInput { // Handle integration type if (item.type === "integration" && item.integration) { return { type: "integration", integration: item.integration, }; } // Handle address type if (item.type === "address") { return { type: "address", address: item.address || "", public: item.public ?? true, }; } // Handle attendeeAddress type if (item.type === "attendeeAddress") { return { type: "attendeeAddress", }; } // Handle link type if (item.type === "link") { return { type: "link", link: item.link || "", public: item.public ?? true, }; } // Handle phone type (organizer phone) if (item.type === "phone") { return { type: "phone", phone: item.phone || "", public: item.public ?? true, }; } // Handle attendeePhone type if (item.type === "attendeePhone") { return { type: "attendeePhone", }; } // Handle attendeeDefined type if (item.type === "attendeeDefined") { return { type: "attendeeDefined", }; } // Fallback return { type: item.type, }; } /** * Get the display name for a location type */ export function getLocationDisplayName(locationType: string, integration?: string): string { // Handle integration type if (locationType === "integration" && integration) { return formatAppIdToDisplayName(integration); } // Check default locations const typeToDisplayName: Record = { address: "In Person (Organizer Address)", attendeeAddress: "In Person (Attendee Address)", link: "Link Meeting", phone: "Organizer Phone Number", attendeePhone: "Attendee Phone Number", attendeeDefined: "Custom Attendee Location", }; return typeToDisplayName[locationType] || locationType; } /** * Get the icon URL for a location type */ export function getLocationIconUrl(locationType: string, integration?: string): string | null { // Handle integration type if (locationType === "integration" && integration) { return getAppIconUrl("", integration); } // Check default location icons const typeToIcon: Record = { address: "https://app.cal.com/map-pin-dark.svg", attendeeAddress: "https://app.cal.com/map-pin-dark.svg", link: "https://app.cal.com/link.svg", phone: "https://app.cal.com/phone.svg", attendeePhone: "https://app.cal.com/phone.svg", attendeeDefined: "https://app.cal.com/message-pin.svg", }; return typeToIcon[locationType] || null; } /** * Create a new LocationItem from a location option selection */ export function createLocationItemFromOption( optionValue: string, optionLabel: string ): LocationItem { const id = generateLocationId(); // Handle integration options (format: "integrations:app-id") if (optionValue.startsWith("integrations:")) { const integration = optionValue.replace("integrations:", ""); return { id, type: "integration", integration, displayName: optionLabel, iconUrl: getAppIconUrl("", integration), }; } // Handle default location types const defaultLocation = defaultLocations.find((loc) => loc.type === optionValue); if (defaultLocation) { const item: LocationItem = { id, type: mapDefaultLocationTypeToApiType(defaultLocation.type), displayName: defaultLocation.label, iconUrl: defaultLocation.iconUrl, }; // Add input fields for specific types if (defaultLocation.type === DefaultLocationType.InPerson) { item.address = ""; item.public = true; } else if (defaultLocation.type === DefaultLocationType.Link) { item.link = ""; item.public = true; } else if (defaultLocation.type === DefaultLocationType.UserPhone) { item.phone = ""; item.public = true; } return item; } // Fallback return { id, type: optionValue as LocationItem["type"], displayName: optionLabel, iconUrl: null, }; } /** * Map DefaultLocationType to API location type */ function mapDefaultLocationTypeToApiType(defaultType: string): LocationItem["type"] { const typeMap: Record = { [DefaultLocationType.AttendeeInPerson]: "attendeeAddress", [DefaultLocationType.InPerson]: "address", [DefaultLocationType.Phone]: "attendeePhone", [DefaultLocationType.UserPhone]: "phone", [DefaultLocationType.Link]: "link", [DefaultLocationType.SomewhereElse]: "attendeeDefined", }; return typeMap[defaultType] || (defaultType as LocationItem["type"]); } /** * Check if a location type requires additional input */ export function locationRequiresInput(locationType: LocationItem["type"]): boolean { return ["address", "link", "phone"].includes(locationType); } /** * Get the input field type for a location */ export function getLocationInputType( locationType: LocationItem["type"] ): "text" | "phone" | "url" | null { switch (locationType) { case "address": return "text"; case "link": return "url"; case "phone": return "phone"; default: return null; } } /** * Get the input placeholder for a location type */ export function getLocationInputPlaceholder(locationType: LocationItem["type"]): string { switch (locationType) { case "address": return "Enter address or place"; case "link": return "https://meet.example.com/join/123456"; case "phone": return "Enter phone number"; default: return ""; } } /** * Get the input label for a location type */ export function getLocationInputLabel(locationType: LocationItem["type"]): string { switch (locationType) { case "address": return "Address"; case "link": return "Meeting Link"; case "phone": return "Phone Number"; default: return ""; } } /** * Validate a location item before saving */ export function validateLocationItem(item: LocationItem): { valid: boolean; error?: string } { // Integration types don't need additional validation if (item.type === "integration") { if (!item.integration) { return { valid: false, error: "Integration type is required" }; } return { valid: true }; } // Address type requires address field if (item.type === "address") { if (!item.address || item.address.trim() === "") { return { valid: false, error: "Address is required" }; } return { valid: true }; } // Link type requires link field if (item.type === "link") { if (!item.link || item.link.trim() === "") { return { valid: false, error: "Meeting link is required" }; } // URL validation with protocol restriction try { const url = new URL(item.link); if (url.protocol !== "http:" && url.protocol !== "https:") { return { valid: false, error: "Meeting link must use http or https" }; } } catch { return { valid: false, error: "Invalid meeting link URL" }; } return { valid: true }; } // Phone type requires phone field if (item.type === "phone") { if (!item.phone || item.phone.trim() === "") { return { valid: false, error: "Phone number is required" }; } return { valid: true }; } // Other types (attendeeAddress, attendeePhone, attendeeDefined) don't need input return { valid: true }; } /** * Build location options for dropdown from conferencing options and default locations */ export function buildLocationOptions( conferencingOptions: Array<{ type: string; appId: string }> ): LocationOptionGroup[] { // Cal Video is always available as the default conferencing app const calVideoOption: LocationOption = { label: "Cal Video", iconUrl: getAppIconUrl("daily_video", "cal-video"), value: "integrations:cal-video", category: "conferencing", }; // Group conferencing apps under "conferencing" category const conferencingAppOptions: LocationOption[] = conferencingOptions .filter((option) => option.appId !== "cal-video" && option.type !== "daily_video") .map((option) => ({ label: formatAppIdToDisplayName(option.appId), iconUrl: getAppIconUrl(option.type, option.appId), value: `integrations:${option.appId}`, category: "conferencing", })); // Group default locations by their category const grouped: Record = { conferencing: [calVideoOption, ...conferencingAppOptions], }; // Add default locations by category defaultLocations.forEach((location) => { const option: LocationOption = { label: location.label, iconUrl: location.iconUrl, value: location.type, category: location.category, }; if (!grouped[location.category]) { grouped[location.category] = []; } grouped[location.category].push(option); }); // Convert to array of groups with proper ordering const categoryOrder = ["conferencing", "in person", "phone", "other"]; const categoryLabels: Record = { conferencing: "Conferencing", "in person": "In Person", phone: "Phone", other: "Other", }; return categoryOrder .filter((category) => grouped[category] && grouped[category].length > 0) .map((category) => ({ category: categoryLabels[category] || category, options: grouped[category], })); } /** * Convert a display name back to a location value for API * Used when selecting a location from a dropdown * * @param displayName - The display name shown in UI (e.g., "Google Meet", "In Person (Organizer Address)") * @param defaultLocationsList - List of default locations to check against * @returns Location value object for API, or null if not found */ export const displayNameToLocationValue = ( displayName: string, defaultLocationsList: Array<{ label: string; type: string }> ): { type: string; integration?: string; address?: string; link?: string; phone?: string; public?: boolean; } | null => { // First check if it's a default location const defaultLocation = defaultLocationsList.find((loc) => loc.label === displayName); if (defaultLocation) { // Map internal location types to API location types switch (defaultLocation.type) { case "attendeeInPerson": return { type: "attendeeAddress" }; case "inPerson": return { type: "address", address: "", public: true }; case "link": return { type: "link", link: "", public: true }; case "phone": return { type: "attendeePhone" }; case "userPhone": return { type: "phone", phone: "", public: true }; default: return null; } } // Check if it's a conferencing app (formatted display name) // e.g., "Google Meet", "Zoom", etc. const appId = displayName.toLowerCase().replace(/\s+/g, "-"); return { type: "integration", integration: appId }; };