This commit is contained in:
Joyce
2026-02-05 17:07:42 -05:00
parent 26e553bfd0
commit 07c11ccd2e
5 changed files with 548 additions and 19 deletions

View File

@@ -0,0 +1,431 @@
import { TimeSlot, Participant } from '@/types/calendar';
import { cn } from '@/lib/utils';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { useState, useMemo, useRef, useEffect } from 'react';
import { Check, X, Loader2, ChevronLeft, ChevronRight, ChevronsRight, Clock, Calendar as CalendarIcon, Sun, Moon, Users } from 'lucide-react';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
const DEFAULT_TIMEZONE = 'America/Toronto';
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri'];
const WORKING_HOUR_START = 8; // 8 AM
const WORKING_HOUR_END = 18; // 6 PM
const ALL_HOURS = Array.from({ length: 24 }, (_, i) => i);
const WORKING_HOURS = Array.from({ length: WORKING_HOUR_END - WORKING_HOUR_START + 1 }, (_, i) => i + WORKING_HOUR_START);
// Helper to check if a slot is in the past or too close (2h buffer)
const isSlotTooSoon = (slotDate: number) => {
const now = Date.now();
const twoHoursFromNow = now + 2 * 60 * 60 * 1000;
return slotDate < twoHoursFromNow;
};
// Reuse previous timezone helpers or simplify
const getWeekDates = (timezone: string, weekOffset: number = 0): Date[] => {
const now = new Date();
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: timezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
});
const todayStr = formatter.format(now);
const [year, month, day] = todayStr.split('-').map(Number);
const todayDate = new Date(year, month - 1, day);
const dayOfWeek = todayDate.getDay();
// If Sunday (0), go back 6 days to Monday. If Mon (1), go back 0. If Sat (6), go back 5.
// Actually standard logic: Mon=1...Sun=7.
// Let's assume standard ISO week start (Mon)
const daysToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
const mondayDate = new Date(year, month - 1, day + daysToMonday + weekOffset * 7);
return Array.from({ length: 5 }, (_, i) => {
const d = new Date(mondayDate);
d.setDate(mondayDate.getDate() + i);
return d;
});
};
const formatTimezoneDisplay = (timezone: string): string => {
try {
const parts = timezone.split('/');
const city = parts[parts.length - 1].replace(/_/g, ' ');
return city;
} catch {
return timezone;
}
};
interface AvailabilityHeatmapV2Props {
slots: TimeSlot[];
selectedParticipants: Participant[];
onSlotSelect: (slot: TimeSlot) => void;
showPartialAvailability?: boolean;
isLoading?: boolean;
weekOffset?: number;
onWeekOffsetChange?: (offset: number) => void;
displayTimezone?: string;
showSecondaryTimezone?: boolean;
secondaryTimezone?: string;
}
export const AvailabilityHeatmapV2 = ({
slots,
selectedParticipants,
onSlotSelect,
showPartialAvailability = false,
isLoading = false,
weekOffset = 0,
onWeekOffsetChange,
displayTimezone = DEFAULT_TIMEZONE,
showSecondaryTimezone = false,
secondaryTimezone = DEFAULT_TIMEZONE,
}: AvailabilityHeatmapV2Props) => {
const [showFullDay, setShowFullDay] = useState(false);
const activeHours = showFullDay ? ALL_HOURS : WORKING_HOURS;
const weekDates = useMemo(() => getWeekDates(displayTimezone, weekOffset), [displayTimezone, weekOffset]);
// Pre-compute slots lookup map for O(1) access
// Key: YYYY-MM-DD:Hour
const slotsMap = useMemo(() => {
const map = new Map<string, TimeSlot>();
slots.forEach(slot => {
// Assuming slot.start_time is ISO string.
// We need to match it to our grid's local time logic.
// This part is "flaky" in the original.
// The slot comes with a specific absolute time.
// We want to place it in the intersection of "Day (in DisplayTZ)" and "Hour (in DisplayTZ)".
const d = new Date(slot.start_time);
// Format to DisplayTZ to find coordinate
const formatter = new Intl.DateTimeFormat('en-CA', {
timeZone: displayTimezone,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: 'numeric',
hour12: false
});
// "2023-10-25, 14"
const parts = formatter.formatToParts(d);
const year = parts.find(p => p.type === 'year')?.value;
const month = parts.find(p => p.type === 'month')?.value;
const day = parts.find(p => p.type === 'day')?.value;
const hour = parts.find(p => p.type === 'hour')?.value;
if (year && month && day && hour) {
// Hour in 24h format might be "24" in some locales? No `hour12: false` gives 0-23 usually.
// But Intl sometimes returns "24"? No, 0-23.
let h = parseInt(hour, 10);
if (h === 24) h = 0; // Just in case
const key = `${year}-${month}-${day}:${h}`;
map.set(key, slot);
}
});
return map;
}, [slots, displayTimezone]);
const formatDateKey = (date: Date) => {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
};
const formatDisplayDate = (date: Date) => {
return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
};
const getSlotForCell = (date: Date, hour: number) => {
const key = `${formatDateKey(date)}:${hour}`;
return slotsMap.get(key);
};
const formatHour = (hour: number) => {
return new Date(0, 0, 0, hour).toLocaleTimeString('en-US', {
hour: 'numeric',
hour12: true,
});
};
if (selectedParticipants.length === 0) {
return (
<div className="flex flex-col items-center justify-center p-12 border-2 border-dashed border-border/50 rounded-xl bg-muted/20 animate-fade-in">
<UsersPlaceholder />
<h3 className="text-xl font-semibold mt-4">No participants selected</h3>
<p className="text-muted-foreground text-center max-w-sm mt-2">
Select team members from the list above to compare calendars and find the perfect meeting time.
</p>
</div>
);
}
return (
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-4 duration-500">
{/* Controls Bar */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-card p-4 rounded-xl border border-border shadow-sm">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => onWeekOffsetChange?.(weekOffset - 1)}
disabled={!onWeekOffsetChange || weekOffset <= 0}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex flex-col items-center min-w-[140px]">
<span className="text-sm font-semibold">
{weekDates[0]?.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
{' - '}
{weekDates[4]?.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
</span>
<span className="text-xs text-muted-foreground">
{weekOffset === 0 ? "This Week" : weekOffset === 1 ? "Next Week" : `${weekOffset} weeks out`}
</span>
</div>
<Button
variant="outline"
size="icon"
className="h-9 w-9"
onClick={() => onWeekOffsetChange?.(weekOffset + 1)}
disabled={!onWeekOffsetChange}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
<div className="h-8 w-px bg-border hidden sm:block" />
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-muted/50 px-3 py-1.5 rounded-md">
<Clock className="w-4 h-4" />
{formatTimezoneDisplay(displayTimezone)}
</div>
</div>
<div className="flex items-center gap-2">
<Button
variant={showFullDay ? "outline" : "secondary"}
size="sm"
onClick={() => setShowFullDay(!showFullDay)}
className="text-xs"
>
{showFullDay ? (
<>
<Sun className="w-3.5 h-3.5 mr-2" />
Show Work Hours
</>
) : (
<>
<Moon className="w-3.5 h-3.5 mr-2" />
Show Full Day
</>
)}
</Button>
</div>
</div>
{/* Main Grid Card */}
<div className="bg-card w-full overflow-hidden rounded-xl border border-border shadow-md">
{isLoading && (
<div className="absolute inset-0 bg-background/50 backdrop-blur-[1px] z-50 flex items-center justify-center">
<Loader2 className="w-10 h-10 animate-spin text-primary" />
</div>
)}
<div className="overflow-auto max-h-[600px] w-full relative">
<div className="min-w-[700px]">
{/* Grid Header */}
<div className="grid grid-cols-[80px_repeat(5,1fr)] sticky top-0 z-30 bg-card border-b border-border shadow-sm">
<div className="sticky left-0 z-40 bg-card text-xs font-semibold text-muted-foreground self-center p-3 text-right border-r border-border/50">
TIME
</div>
{weekDates.map(date => {
const isToday = new Date().toDateString() === date.toDateString();
return (
<div key={date.toISOString()} className={cn(
"text-center p-3 transition-colors border-r border-border/30 last:border-0",
isToday ? "bg-primary/5 text-primary font-bold" : "text-foreground"
)}>
<div className="text-sm">{date.toLocaleDateString('en-US', { weekday: 'short' })}</div>
<div className={cn("text-2xl", isToday ? "font-bold" : "font-light")}>
{date.getDate()}
</div>
</div>
);
})}
</div>
{/* Grid Body */}
<div className="relative">
{activeHours.map((hour) => {
const isNight = hour < 8 || hour >= 18;
return (
<div
key={hour}
className={cn(
"grid grid-cols-[80px_repeat(5,1fr)] group items-stretch transition-colors border-b border-border/30 last:border-0",
isNight ? "bg-muted/30" : "bg-card",
"hover:bg-muted/10"
)}
>
{/* Time Label - Sticky Left */}
<div className={cn(
"text-xs text-muted-foreground font-medium text-right pr-4 py-3 flex items-center justify-end gap-1.5",
"sticky left-0 z-20 border-r border-border/50",
isNight ? "bg-muted/30 backdrop-blur-md" : "bg-card"
)}>
{isNight ? (
<Moon className="w-3 h-3 text-slate-400/50" />
) : (
<Sun className="w-3 h-3 text-amber-500/50" />
)}
{formatHour(hour)}
</div>
{/* Days */}
{weekDates.map(date => {
const slot = getSlotForCell(date, hour);
const tooSoon = slot ? isSlotTooSoon(new Date(slot.start_time).getTime()) : true;
// Availability logic
const availability = slot?.availability || 'none';
const isNone = availability === 'none';
const isPartial = availability === 'partial' && showPartialAvailability;
const isFull = availability === 'full';
const isPartialHidden = availability === 'partial' && !showPartialAvailability;
// Styling
let bgClass = ""; // Default transparent
if (!slot) bgClass = "bg-muted/10 pattern-diagonal-lines opacity-50";
else if (isFull) bgClass = "bg-emerald-500/90 hover:bg-emerald-600 shadow-sm";
else if (isPartial) bgClass = "bg-amber-400/80 hover:bg-amber-500 shadow-sm";
else if (isNone || isPartialHidden) bgClass = "bg-transparent hover:bg-muted/20";
if (tooSoon && slot) bgClass = cn(bgClass, "opacity-30 cursor-not-allowed pattern-diagonal-lines");
return (
<div key={date.toISOString()} className="p-1 h-[52px] border-r border-border/30 last:border-0">
{slot ? (
<Popover>
<PopoverTrigger asChild disabled={tooSoon}>
<div className={cn(
"w-full h-full rounded-md transition-all duration-200 cursor-pointer flex items-center justify-center group/cell relative overflow-hidden",
bgClass,
!tooSoon && (isFull || isPartial) ? "scale-[0.98] hover:scale-100 hover:ring-2 ring-primary/20" : ""
)}>
{/* Mini Indicators for color-blind accessibility or density */}
{isFull && <Check className="w-4 h-4 text-white opacity-0 group-hover/cell:opacity-100 transition-opacity" />}
</div>
</PopoverTrigger>
<PopoverContent className="w-72 p-0 rounded-xl overflow-hidden shadow-xl border-border" side="right" align="start">
<div className="p-4 bg-muted/30 border-b border-border/50">
<h4 className="font-semibold text-base flex items-center gap-2">
<CalendarIcon className="w-4 h-4 text-muted-foreground" />
{formatDisplayDate(date)}
</h4>
<div className="text-sm text-muted-foreground mt-1 flex items-center gap-2">
<Clock className="w-4 h-4" />
{formatHour(hour)} - {formatHour(hour + 1)}
</div>
</div>
<div className="p-4 space-y-3 max-h-[300px] overflow-y-auto">
<div className="space-y-2">
{selectedParticipants.map(participant => {
const isAvailable = slot.availableParticipants.includes(participant.name);
return (
<div key={participant.id} className="flex items-center justify-between text-sm p-2 rounded-lg hover:bg-muted/50 transition-colors">
<div className="flex items-center gap-3">
<div className={cn(
"w-2 h-2 rounded-full",
isAvailable ? "bg-emerald-500" : "bg-destructive"
)} />
<span className={cn(!isAvailable && "text-muted-foreground")}>
{participant.name}
</span>
</div>
{isAvailable ? (
<Check className="w-4 h-4 text-emerald-500" />
) : (
<span className="text-xs text-muted-foreground">Busy</span>
)}
</div>
);
})}
</div>
{(!isNone && !tooSoon) && (
<Button
className="w-full mt-4 bg-emerald-600 hover:bg-emerald-700 text-white"
onClick={() => onSlotSelect(slot)}
>
Schedule Meeting
</Button>
)}
</div>
</PopoverContent>
</Popover>
) : (
// Placeholder for missing slots
<div className="w-full h-full rounded-md bg-muted/5 border border-dashed border-border/30"></div>
)}
</div>
);
})}
</div>
);
})}
</div>
</div>
</div>
{/* Legend */}
<div className="flex flex-wrap items-center justify-center bg-muted/20 border-t p-3 gap-6 text-sm">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-emerald-500 shadow-sm"></div>
<span className="font-medium text-foreground">All Available</span>
</div>
{showPartialAvailability && (
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-amber-400 shadow-sm"></div>
<span className="font-medium text-foreground">Partial Match</span>
</div>
)}
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded border border-slate-300 bg-slate-100 dark:bg-slate-800 dark:border-slate-700"></div>
<span className="text-muted-foreground">Busy / No Match</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded bg-muted/30 pattern-diagonal-lines ring-1 ring-border/50"></div>
<span className="text-muted-foreground">Past / Too Soon</span>
</div>
</div>
</div>
</div>
);
};
function UsersPlaceholder() {
return (
<div className="relative w-16 h-16 mb-2">
<div className="absolute top-0 left-0 w-10 h-10 rounded-full bg-muted border-2 border-background flex items-center justify-center">
<Users className="w-5 h-5 text-muted-foreground" />
</div>
<div className="absolute bottom-0 right-0 w-10 h-10 rounded-full bg-muted border-2 border-background flex items-center justify-center">
<Users className="w-5 h-5 text-muted-foreground" />
</div>
</div>
);
}
// Importing Users icon since it was missing in imports

View File

@@ -1,7 +1,27 @@
import { Calendar } from 'lucide-react'; import { Calendar, Moon, Sun } from 'lucide-react';
import { getAvatarColor } from '@/lib/utils'; import { getAvatarColor } from '@/lib/utils';
import { useState, useEffect } from 'react';
import { Button } from './ui/button';
export const Header = () => { export const Header = () => {
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
if (typeof window !== 'undefined') {
return localStorage.getItem('theme') === 'dark' ? 'dark' : 'light';
}
return 'light';
});
useEffect(() => {
const root = window.document.documentElement;
root.classList.remove('light', 'dark');
root.classList.add(theme);
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return ( return (
<header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-50"> <header className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-50">
<div className="container max-w-5xl mx-auto px-4 py-4 flex items-center justify-between"> <div className="container max-w-5xl mx-auto px-4 py-4 flex items-center justify-between">
@@ -14,7 +34,15 @@ export const Header = () => {
<p className="text-xs text-muted-foreground">Calendar Coordination</p> <p className="text-xs text-muted-foreground">Calendar Coordination</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={toggleTheme} className="rounded-full">
{theme === 'light' ? (
<Sun className="h-5 w-5 text-amber-500" />
) : (
<Moon className="h-5 w-5 text-slate-400" />
)}
<span className="sr-only">Toggle theme</span>
</Button>
<div <div
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium text-white" className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium text-white"
style={{ backgroundColor: getAvatarColor("AR") }} style={{ backgroundColor: getAvatarColor("AR") }}

View File

@@ -111,7 +111,7 @@ export const ParticipantSelector = ({
/> />
{isDropdownOpen && filteredParticipants.length > 0 && ( {isDropdownOpen && filteredParticipants.length > 0 && (
<div className="absolute z-10 w-full mt-2 bg-popover border border-border rounded-lg shadow-popover animate-scale-in overflow-hidden"> <div className="absolute z-50 w-full mt-2 bg-popover border border-border rounded-lg shadow-popover animate-scale-in overflow-hidden">
{filteredParticipants.map((participant, index) => ( {filteredParticipants.map((participant, index) => (
<button <button
key={participant.id} key={participant.id}

View File

@@ -111,10 +111,51 @@
--sidebar-ring: 18 68% 51%; --sidebar-ring: 18 68% 51%;
} }
/* Dark mode intentionally removed/reset to match light mode system for now, /* Dark mode */
or you can define a proper dark mode if required. .dark {
Keeping it simple as per previous apps. */ --background: 60 5% 8%; /* #161614 */
} --foreground: 60 9% 93%; /* #F0F0EC */
--card: 60 5% 10%; /* Slightly lighter than bg */
--card-foreground: 60 9% 93%;
--popover: 60 5% 10%;
--popover-foreground: 60 9% 93%;
--primary: 18 68% 51%; /* Keep primary brand color */
--primary-foreground: 60 9% 97%;
--secondary: 60 5% 15%;
--secondary-foreground: 60 9% 93%;
--muted: 60 5% 15%;
--muted-foreground: 60 5% 65%;
--accent: 60 5% 15%;
--accent-foreground: 60 9% 93%;
--destructive: 0 62% 30%;
--destructive-foreground: 60 9% 97%;
--border: 60 5% 20%;
--input: 60 5% 20%;
--ring: 18 68% 51%;
/* Dark mode availability colors */
--availability-full: 142 70% 45%; /* Brighter green for dark mode */
--availability-partial: 25 80% 65%; /* Same orange */
--availability-none: 60 5% 20%; /* Darker grey */
--sidebar-background: 60 5% 8%;
--sidebar-foreground: 60 9% 93%;
--sidebar-primary: 18 68% 51%;
--sidebar-primary-foreground: 60 9% 97%;
--sidebar-accent: 60 5% 15%;
--sidebar-accent-foreground: 60 9% 93%;
--sidebar-border: 60 5% 20%;
--sidebar-ring: 18 68% 51%;
}
}
@layer base { @layer base {
* { * {

View File

@@ -4,6 +4,7 @@ import { Header } from '@/components/Header';
import { ParticipantSelector } from '@/components/ParticipantSelector'; import { ParticipantSelector } from '@/components/ParticipantSelector';
import { ParticipantManager } from '@/components/ParticipantManager'; import { ParticipantManager } from '@/components/ParticipantManager';
import { AvailabilityHeatmap } from '@/components/AvailabilityHeatmap'; import { AvailabilityHeatmap } from '@/components/AvailabilityHeatmap';
import { AvailabilityHeatmapV2 } from '@/components/AvailabilityHeatmapV2';
import { ScheduleModal } from '@/components/ScheduleModal'; import { ScheduleModal } from '@/components/ScheduleModal';
import { TimezoneSelector } from '@/components/TimezoneSelector'; import { TimezoneSelector } from '@/components/TimezoneSelector';
import { Participant, TimeSlot } from '@/types/calendar'; import { Participant, TimeSlot } from '@/types/calendar';
@@ -89,6 +90,7 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
const [availabilitySlots, setAvailabilitySlots] = useState<TimeSlot[]>([]); const [availabilitySlots, setAvailabilitySlots] = useState<TimeSlot[]>([]);
const [selectedSlot, setSelectedSlot] = useState<TimeSlot | null>(null); const [selectedSlot, setSelectedSlot] = useState<TimeSlot | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [useRedesign, setUseRedesign] = useState(false);
const [settings, setSettings] = useState<SettingsState>(defaultSettings); const [settings, setSettings] = useState<SettingsState>(defaultSettings);
const [weekOffset, setWeekOffset] = useState(0); const [weekOffset, setWeekOffset] = useState(0);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -323,6 +325,18 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
<PopoverContent className="w-80" align="end"> <PopoverContent className="w-80" align="end">
<div className="space-y-4"> <div className="space-y-4">
<h4 className="font-medium">Settings</h4> <h4 className="font-medium">Settings</h4>
<div className="flex items-center justify-between gap-4 pb-4 border-b border-border">
<Label htmlFor="use-redesign" className="text-sm cursor-pointer font-medium text-primary">
Try New Design
</Label>
<Switch
id="use-redesign"
checked={useRedesign}
onCheckedChange={setUseRedesign}
/>
</div>
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<Label htmlFor="partial-availability" className="text-sm cursor-pointer"> <Label htmlFor="partial-availability" className="text-sm cursor-pointer">
Show partial availability Show partial availability
@@ -436,18 +450,33 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
/> />
</div> </div>
<AvailabilityHeatmap {useRedesign ? (
slots={availabilitySlots} <AvailabilityHeatmapV2
selectedParticipants={selectedParticipants} slots={availabilitySlots}
onSlotSelect={handleSlotSelect} selectedParticipants={selectedParticipants}
showPartialAvailability={settings.showPartialAvailability} onSlotSelect={handleSlotSelect}
isLoading={isLoading} showPartialAvailability={settings.showPartialAvailability}
weekOffset={weekOffset} isLoading={isLoading}
onWeekOffsetChange={setWeekOffset} weekOffset={weekOffset}
displayTimezone={settings.displayTimezone} onWeekOffsetChange={setWeekOffset}
showSecondaryTimezone={settings.showSecondaryTimezone} displayTimezone={settings.displayTimezone}
secondaryTimezone={settings.secondaryTimezone} showSecondaryTimezone={settings.showSecondaryTimezone}
/> secondaryTimezone={settings.secondaryTimezone}
/>
) : (
<AvailabilityHeatmap
slots={availabilitySlots}
selectedParticipants={selectedParticipants}
onSlotSelect={handleSlotSelect}
showPartialAvailability={settings.showPartialAvailability}
isLoading={isLoading}
weekOffset={weekOffset}
onWeekOffsetChange={setWeekOffset}
displayTimezone={settings.displayTimezone}
showSecondaryTimezone={settings.showSecondaryTimezone}
secondaryTimezone={settings.secondaryTimezone}
/>
)}
</> </>
)} )}
</div> </div>