import { useAppDispatch, useAppSelector } from "@/app/hooks"; import { removeTempCal } from "@/features/Calendars/CalendarSlice"; import { Calendar } from "@/features/Calendars/CalendarTypes"; import { getTempCalendarsListAsync } from "@/features/Calendars/services"; import { setView } from "@/features/Settings/SettingsSlice"; import { TextField, useTheme } from "@linagora/twake-mui"; import { useRef } from "react"; import { useI18n } from "twake-i18n"; import { PeopleSearch, User } from "../Attendees/PeopleSearch"; import { getAccessiblePair } from "@/utils/getAccessiblePair"; const requestControllers = new Map(); export function TempCalendarsInput({ tempUsers, setTempUsers, handleToggleEventPreview, }: { tempUsers: User[]; setTempUsers: (users: User[]) => void; handleToggleEventPreview: () => void; }) { const dispatch = useAppDispatch(); const tempcalendars = useAppSelector((state) => state.calendars.templist) ?? {}; const theme = useTheme(); const { t } = useI18n(); const prevUsersRef = useRef([]); const userColorsRef = useRef(new Map()); const handleUserChange = async (_: React.SyntheticEvent, users: User[]) => { setTempUsers(users); const prevUsers = prevUsersRef.current; const addedUsers = users.filter( (u) => !prevUsers.some((p) => p.email === u.email) ); const removedUsers = prevUsers.filter( (p) => !users.some((u) => u.email === p.email) ); prevUsersRef.current = users; if (addedUsers.length > 0) { dispatch(setView("calendar")); for (const user of addedUsers) { const controller = new AbortController(); requestControllers.set(user.email, controller); if (!userColorsRef.current.has(user.email)) { const existingColors = Array.from(userColorsRef.current.values()); const lightColor = generateDistinctColor(existingColors); userColorsRef.current.set(user.email, lightColor); } const lightColor = userColorsRef.current.get(user.email)!; user.color = { light: lightColor, dark: getAccessiblePair(lightColor, theme), }; dispatch( getTempCalendarsListAsync(user, { signal: controller.signal }) ); } } for (const user of removedUsers) { const controller = requestControllers.get(user.email); if (controller) { controller.abort(); requestControllers.delete(user.email); } const calIds = buildEmailToCalendarMap(tempcalendars).get(user.email); calIds?.forEach((id) => dispatch(removeTempCal(id))); userColorsRef.current.delete(user.email); } }; return ( ( 0 ? { "& .MuiOutlinedInput-root": { flexDirection: "column", alignItems: "start", "& .MuiInputBase-input": { width: "100%", }, }, } : undefined } /> )} /> ); } function buildEmailToCalendarMap(calRecord: Record) { const map = new Map(); for (const [id, cal] of Object.entries(calRecord)) { cal.owner?.emails?.forEach((email) => { const existing = map.get(email); if (existing) { existing.push(id); } else { map.set(email, [id]); } }); } return map; } function extractHSL(hslColor: string): { h: number; s: number; l: number } { const match = hslColor.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/); if (!match) return { h: 0, s: 70, l: 50 }; return { h: parseInt(match[1], 10), s: parseInt(match[2], 10), l: parseInt(match[3], 10), }; } function getHueDistance(hue1: number, hue2: number): number { const diff = Math.abs(hue1 - hue2); return Math.min(diff, 360 - diff); } function getColorDistance(color1: string, color2: string): number { const hsl1 = extractHSL(color1); const hsl2 = extractHSL(color2); const hueDist = getHueDistance(hsl1.h, hsl2.h) / 180; const satDist = Math.abs(hsl1.s - hsl2.s) / 100; const lightDist = Math.abs(hsl1.l - hsl2.l) / 100; // Weighted distance: hue is most important, then saturation, then lightness return Math.sqrt( hueDist * hueDist * 4 + satDist * satDist * 1 + lightDist * lightDist * 1 ); } function generateDistinctColor( existingColors: string[], minDistance: number = 0.35, maxAttempts: number = 150 ): string { // Predefined palette with good variety const palette = [ { h: 0, s: 75, l: 50 }, // Red { h: 30, s: 80, l: 50 }, // Orange { h: 50, s: 85, l: 50 }, // Yellow-orange { h: 120, s: 70, l: 45 }, // Green { h: 180, s: 70, l: 45 }, // Cyan { h: 210, s: 75, l: 50 }, // Blue { h: 270, s: 70, l: 50 }, // Purple { h: 320, s: 75, l: 50 }, // Magenta { h: 90, s: 65, l: 55 }, // Lime { h: 200, s: 80, l: 45 }, // Deep blue { h: 290, s: 65, l: 55 }, // Light purple { h: 340, s: 75, l: 50 }, // Pink-red ]; // Try palette colors first for (const color of palette) { const colorStr = `hsl(${color.h}, ${color.s}%, ${color.l}%)`; if (existingColors.length === 0) { return colorStr; } const minDist = Math.min( ...existingColors.map((c) => getColorDistance(colorStr, c)) ); if (minDist >= minDistance) { return colorStr; } } // If palette exhausted, generate random colors let bestColor = `hsl(0, 70%, 50%)`; let bestMinDistance = 0; for (let attempt = 0; attempt < maxAttempts; attempt++) { const h = Math.floor(Math.random() * 360); const s = 65 + Math.floor(Math.random() * 20); // 65-85% const l = 45 + Math.floor(Math.random() * 15); // 45-60% const candidateColor = `hsl(${h}, ${s}%, ${l}%)`; const minDist = Math.min( ...existingColors.map((c) => getColorDistance(candidateColor, c)) ); if (minDist >= minDistance) { return candidateColor; } if (minDist > bestMinDistance) { bestMinDistance = minDist; bestColor = candidateColor; } } return bestColor; }