update #9

Merged
Joyce merged 1 commits from implement-more-feeedback into main 2026-02-06 01:36:02 +00:00
5 changed files with 257 additions and 162 deletions

View File

@@ -156,6 +156,23 @@ export const AvailabilityHeatmapV2 = ({
}); });
}; };
// Move hooks to top level to avoid conditional hook execution error
const tzOffsetDiff = useMemo(() => {
try {
const now = new Date();
const p = parseInt(new Intl.DateTimeFormat('en-US', { timeZone: displayTimezone, hour: 'numeric', hour12: false }).format(now));
const s = parseInt(new Intl.DateTimeFormat('en-US', { timeZone: secondaryTimezone, hour: 'numeric', hour12: false }).format(now));
let diff = s - p;
if (diff > 12) diff -= 24;
if (diff < -12) diff += 24;
return diff;
} catch {
return 0;
}
}, [displayTimezone, secondaryTimezone]);
const timeColWidth = showSecondaryTimezone ? "120px" : "80px";
if (selectedParticipants.length === 0) { if (selectedParticipants.length === 0) {
return ( 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"> <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">
@@ -245,9 +262,15 @@ export const AvailabilityHeatmapV2 = ({
<div className="overflow-auto max-h-[600px] w-full relative"> <div className="overflow-auto max-h-[600px] w-full relative">
<div className="min-w-[700px]"> <div className="min-w-[700px]">
{/* Grid Header */} {/* 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
<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"> className="grid sticky top-0 z-30 bg-card border-b border-border shadow-sm"
TIME style={{ gridTemplateColumns: `${timeColWidth} repeat(5, 1fr)` }}
>
<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 flex flex-col items-end gap-1">
<span>{formatTimezoneDisplay(displayTimezone)}</span>
{showSecondaryTimezone && (
<span className="text-[10px] text-muted-foreground/60 font-normal">{formatTimezoneDisplay(secondaryTimezone)}</span>
)}
</div> </div>
{weekDates.map(date => { {weekDates.map(date => {
const isToday = new Date().toDateString() === date.toDateString(); const isToday = new Date().toDateString() === date.toDateString();
@@ -269,27 +292,41 @@ export const AvailabilityHeatmapV2 = ({
<div className="relative"> <div className="relative">
{activeHours.map((hour) => { {activeHours.map((hour) => {
const isNight = hour < 8 || hour >= 18; const isNight = hour < 8 || hour >= 18;
// Calculate secondary time
let secondaryHour = hour + tzOffsetDiff;
if (secondaryHour >= 24) secondaryHour -= 24;
if (secondaryHour < 0) secondaryHour += 24;
return ( return (
<div <div
key={hour} key={hour}
className={cn( className={cn(
"grid grid-cols-[80px_repeat(5,1fr)] group items-stretch transition-colors border-b border-border/30 last:border-0", "grid group items-stretch transition-colors border-b border-border/30 last:border-0",
isNight ? "bg-muted/30" : "bg-card", isNight ? "bg-muted/30" : "bg-card",
"hover:bg-muted/10" "hover:bg-muted/10"
)} )}
style={{ gridTemplateColumns: `${timeColWidth} repeat(5, 1fr)` }}
> >
{/* Time Label - Sticky Left */} {/* Time Label - Sticky Left */}
<div className={cn( <div className={cn(
"text-xs text-muted-foreground font-medium text-right pr-4 py-3 flex items-center justify-end gap-1.5", "text-xs text-muted-foreground font-medium text-right pr-4 py-3 flex flex-col items-end justify-center gap-0.5",
"sticky left-0 z-20 border-r border-border/50", "sticky left-0 z-20 border-r border-border/50",
isNight ? "bg-muted/30 backdrop-blur-md" : "bg-card" isNight ? "bg-muted/30 backdrop-blur-md" : "bg-card"
)}> )}>
<div className="flex items-center gap-1.5">
{isNight ? ( {isNight ? (
<Moon className="w-3 h-3 text-slate-400/50" /> <Moon className="w-3 h-3 text-slate-400/50" />
) : ( ) : (
<Sun className="w-3 h-3 text-amber-500/50" /> <Sun className="w-3 h-3 text-amber-500/50" />
)} )}
{formatHour(hour)} <span>{formatHour(hour)}</span>
</div>
{showSecondaryTimezone && (
<span className="text-[10px] text-muted-foreground/60 font-mono">
{formatHour(secondaryHour)}
</span>
)}
</div> </div>
{/* Days */} {/* Days */}

View File

@@ -145,6 +145,20 @@ export const ParticipantSelector = ({
className="flex-1 min-w-[100px] h-7 border-none shadow-none focus-visible:ring-0 p-0 text-sm bg-transparent placeholder:text-muted-foreground/70" className="flex-1 min-w-[100px] h-7 border-none shadow-none focus-visible:ring-0 p-0 text-sm bg-transparent placeholder:text-muted-foreground/70"
/> />
{isDropdownOpen && (
<button
onClick={(e) => {
e.stopPropagation();
setIsDropdownOpen(false);
inputRef.current?.blur();
}}
className="p-1 hover:bg-muted rounded-full transition-colors ml-1"
title="Close"
>
<X className="w-4 h-4 text-muted-foreground" />
</button>
)}
{isDropdownOpen && filteredParticipants.length > 0 && ( {isDropdownOpen && filteredParticipants.length > 0 && (
<div className="absolute top-full left-0 z-50 w-full mt-2 bg-popover border border-border rounded-lg shadow-popover animate-scale-in overflow-hidden"> <div className="absolute top-full left-0 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) => (

View File

@@ -11,11 +11,12 @@ interface TimezoneSelectorProps {
// Get all IANA timezones // Get all IANA timezones
const getAllTimezones = (): string[] => { const getAllTimezones = (): string[] => {
let timezones: string[] = [];
try { try {
return Intl.supportedValuesOf('timeZone'); timezones = Intl.supportedValuesOf('timeZone');
} catch { } catch {
// Fallback for older browsers // Fallback for older browsers
return [ timezones = [
'UTC', 'UTC',
'America/New_York', 'America/New_York',
'America/Chicago', 'America/Chicago',
@@ -23,6 +24,7 @@ const getAllTimezones = (): string[] => {
'America/Los_Angeles', 'America/Los_Angeles',
'America/Toronto', 'America/Toronto',
'America/Vancouver', 'America/Vancouver',
'America/Montreal',
'Europe/London', 'Europe/London',
'Europe/Paris', 'Europe/Paris',
'Europe/Berlin', 'Europe/Berlin',
@@ -33,6 +35,17 @@ const getAllTimezones = (): string[] => {
'Pacific/Auckland', 'Pacific/Auckland',
]; ];
} }
// Prioritize Montreal as requested
const priorityTimezone = 'America/Montreal';
if (!timezones.includes(priorityTimezone)) {
timezones.push(priorityTimezone);
}
return [
priorityTimezone,
...timezones.filter((tz) => tz !== priorityTimezone),
];
}; };
// Get UTC offset for a timezone // Get UTC offset for a timezone
@@ -158,7 +171,7 @@ export const TimezoneSelector = ({
No timezones found No timezones found
</div> </div>
) : ( ) : (
filteredTimezones.slice(0, 50).map((timezone) => { filteredTimezones.map((timezone) => {
const isSelected = timezone === value; const isSelected = timezone === value;
const offset = getTimezoneOffset(timezone); const offset = getTimezoneOffset(timezone);
const label = formatTimezoneLabel(timezone); const label = formatTimezoneLabel(timezone);
@@ -204,12 +217,6 @@ export const TimezoneSelector = ({
}) })
)} )}
</div> </div>
{filteredTimezones.length > 50 && (
<div className="px-4 py-2 text-xs text-muted-foreground text-center border-t border-border">
Showing 50 of {filteredTimezones.length} results
</div>
)}
</div> </div>
)} )}
</div> </div>

View File

@@ -1,17 +1,17 @@
import * as SheetPrimitive from "@radix-ui/react-dialog"; import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"; import * as SheetPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"; import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"; import { X } from "lucide-react"
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root; const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger; const SheetTrigger = SheetPrimitive.Trigger
const SheetClose = SheetPrimitive.Close; const SheetClose = SheetPrimitive.Close
const SheetPortal = SheetPrimitive.Portal; const SheetPortal = SheetPrimitive.Portal
const SheetOverlay = React.forwardRef< const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>, React.ElementRef<typeof SheetPrimitive.Overlay>,
@@ -20,13 +20,13 @@ const SheetOverlay = React.forwardRef<
<SheetPrimitive.Overlay <SheetPrimitive.Overlay
className={cn( className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className, className
)} )}
{...props} {...props}
ref={ref} ref={ref}
/> />
)); ))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva( const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
@@ -44,64 +44,95 @@ const sheetVariants = cva(
defaultVariants: { defaultVariants: {
side: "right", side: "right",
}, },
}, }
); )
interface SheetContentProps interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>, extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> { } VariantProps<typeof sheetVariants> { }
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>( const SheetContent = React.forwardRef<
({ side = "right", className, children, ...props }, ref) => ( React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal> <SheetPortal>
<SheetOverlay /> <SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}> <SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{children} {children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity data-[state=open]:bg-secondary hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"> <SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" /> <X className="h-4 w-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</SheetPrimitive.Close> </SheetPrimitive.Close>
</SheetPrimitive.Content> </SheetPrimitive.Content>
</SheetPortal> </SheetPortal>
), ))
); SheetContent.displayName = SheetPrimitive.Content.displayName
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const SheetHeader = ({
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} /> className,
); ...props
SheetHeader.displayName = "SheetHeader"; }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => ( const SheetFooter = ({
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} /> className,
); ...props
SheetFooter.displayName = "SheetFooter"; }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef< const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>, React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title> React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} /> <SheetPrimitive.Title
)); ref={ref}
SheetTitle.displayName = SheetPrimitive.Title.displayName; className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef< const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>, React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description> React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => ( >(({ className, ...props }, ref) => (
<SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} /> <SheetPrimitive.Description
)); ref={ref}
SheetDescription.displayName = SheetPrimitive.Description.displayName; className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export { export {
Sheet, Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose, SheetClose,
SheetContent, SheetContent,
SheetDescription,
SheetFooter,
SheetHeader, SheetHeader,
SheetOverlay, SheetFooter,
SheetPortal,
SheetTitle, SheetTitle,
SheetTrigger, SheetDescription,
}; }

View File

@@ -16,6 +16,11 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover'; } from '@/components/ui/popover';
import {
Sheet,
SheetContent,
SheetTrigger,
} from '@/components/ui/sheet';
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -62,8 +67,8 @@ interface SettingsState {
const defaultSettings: SettingsState = { const defaultSettings: SettingsState = {
showPartialAvailability: false, showPartialAvailability: false,
displayTimezone: getUserTimezone(), displayTimezone: getUserTimezone(),
showSecondaryTimezone: false, showSecondaryTimezone: true,
secondaryTimezone: 'America/Toronto', // Company timezone as default secondary secondaryTimezone: 'America/Montreal', // Company timezone as default secondary
}; };
function apiToParticipant(p: ParticipantAPI): Participant { function apiToParticipant(p: ParticipantAPI): Participant {
@@ -90,7 +95,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 [useRedesign, setUseRedesign] = useState(true);
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);
@@ -316,16 +321,22 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
> >
<RefreshCw className={`w-5 h-5 ${isSyncing ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-5 h-5 ${isSyncing ? 'animate-spin' : ''}`} />
</Button> </Button>
<Popover> <Sheet>
<PopoverTrigger asChild> <SheetTrigger asChild>
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<Settings className="w-5 h-5" /> <Settings className="w-5 h-5" />
</Button> </Button>
</PopoverTrigger> </SheetTrigger>
<PopoverContent className="w-80" align="end"> <SheetContent side="right" className="w-[300px] sm:w-[350px]">
<div className="space-y-4"> <div className="space-y-6 py-4">
<h4 className="font-medium">Settings</h4> <div className="space-y-2">
<h4 className="font-semibold text-lg tracking-tight">Settings</h4>
<p className="text-sm text-muted-foreground">
Configure your calendar preferences.
</p>
</div>
<div className="space-y-4">
<div className="flex items-center justify-between gap-4 pb-4 border-b border-border"> <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"> <Label htmlFor="use-redesign" className="text-sm cursor-pointer font-medium text-primary">
Try New Design Try New Design
@@ -338,9 +349,14 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
</div> </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"> <div className="space-y-0.5">
Show partial availability <Label htmlFor="partial-availability" className="text-sm font-medium cursor-pointer">
Partial Availability
</Label> </Label>
<p className="text-xs text-muted-foreground">
Show slots where some are busy
</p>
</div>
<Switch <Switch
id="partial-availability" id="partial-availability"
checked={settings.showPartialAvailability} checked={settings.showPartialAvailability}
@@ -349,11 +365,8 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
} }
/> />
</div> </div>
<p className="text-xs text-muted-foreground">
When enabled, shows time slots where only some participants are available.
</p>
<div className="border-t border-border pt-4"> <div className="border-t border-border pt-4 space-y-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<Label htmlFor="secondary-timezone" className="text-sm cursor-pointer"> <Label htmlFor="secondary-timezone" className="text-sm cursor-pointer">
Show secondary timezone Show secondary timezone
@@ -367,8 +380,8 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
/> />
</div> </div>
{settings.showSecondaryTimezone && ( {settings.showSecondaryTimezone && (
<div className="mt-3"> <div className="space-y-2">
<Label className="text-xs text-muted-foreground mb-2 block"> <Label className="text-xs text-muted-foreground block">
Secondary timezone Secondary timezone
</Label> </Label>
<TimezoneSelector <TimezoneSelector
@@ -377,9 +390,6 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
/> />
</div> </div>
)} )}
<p className="text-xs text-muted-foreground mt-2">
Display times in two timezones side by side.
</p>
</div> </div>
<div className="border-t border-border pt-4"> <div className="border-t border-border pt-4">
@@ -397,9 +407,7 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle>Clear all bookings?</AlertDialogTitle> <AlertDialogTitle>Clear all bookings?</AlertDialogTitle>
<AlertDialogDescription> <AlertDialogDescription>
This will remove all scheduled meetings from the system. This will remove all scheduled meetings. This cannot be undone.
This action cannot be undone. Calendar invites already
sent will not be affected.
</AlertDialogDescription> </AlertDialogDescription>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogFooter> <AlertDialogFooter>
@@ -413,13 +421,11 @@ const Index = ({ defaultTab = 'schedule' }: IndexProps) => {
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<p className="text-xs text-muted-foreground mt-2">
Remove all scheduled meetings from the system.
</p>
</div> </div>
</div> </div>
</PopoverContent> </div>
</Popover> </SheetContent>
</Sheet>
</div> </div>
<h2 className="text-3xl font-bold text-foreground mb-2"> <h2 className="text-3xl font-bold text-foreground mb-2">
Schedule a Meeting Schedule a Meeting