Merge pull request 'update' (#9) from implement-more-feeedback into main

Reviewed-on: #9
This commit was merged in pull request #9.
This commit is contained in:
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) {
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">
@@ -245,9 +262,15 @@ export const AvailabilityHeatmapV2 = ({
<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
className="grid sticky top-0 z-30 bg-card border-b border-border shadow-sm"
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>
{weekDates.map(date => {
const isToday = new Date().toDateString() === date.toDateString();
@@ -269,27 +292,41 @@ export const AvailabilityHeatmapV2 = ({
<div className="relative">
{activeHours.map((hour) => {
const isNight = hour < 8 || hour >= 18;
// Calculate secondary time
let secondaryHour = hour + tzOffsetDiff;
if (secondaryHour >= 24) secondaryHour -= 24;
if (secondaryHour < 0) secondaryHour += 24;
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",
"grid group items-stretch transition-colors border-b border-border/30 last:border-0",
isNight ? "bg-muted/30" : "bg-card",
"hover:bg-muted/10"
)}
style={{ gridTemplateColumns: `${timeColWidth} repeat(5, 1fr)` }}
>
{/* 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",
"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",
isNight ? "bg-muted/30 backdrop-blur-md" : "bg-card"
)}>
<div className="flex items-center gap-1.5">
{isNight ? (
<Moon className="w-3 h-3 text-slate-400/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>
{/* 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"
/>
{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 && (
<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) => (

View File

@@ -11,11 +11,12 @@ interface TimezoneSelectorProps {
// Get all IANA timezones
const getAllTimezones = (): string[] => {
let timezones: string[] = [];
try {
return Intl.supportedValuesOf('timeZone');
timezones = Intl.supportedValuesOf('timeZone');
} catch {
// Fallback for older browsers
return [
timezones = [
'UTC',
'America/New_York',
'America/Chicago',
@@ -23,6 +24,7 @@ const getAllTimezones = (): string[] => {
'America/Los_Angeles',
'America/Toronto',
'America/Vancouver',
'America/Montreal',
'Europe/London',
'Europe/Paris',
'Europe/Berlin',
@@ -33,6 +35,17 @@ const getAllTimezones = (): string[] => {
'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
@@ -158,7 +171,7 @@ export const TimezoneSelector = ({
No timezones found
</div>
) : (
filteredTimezones.slice(0, 50).map((timezone) => {
filteredTimezones.map((timezone) => {
const isSelected = timezone === value;
const offset = getTimezoneOffset(timezone);
const label = formatTimezoneLabel(timezone);
@@ -204,12 +217,6 @@ export const TimezoneSelector = ({
})
)}
</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>

View File

@@ -1,17 +1,17 @@
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import * as React from "react";
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { cva, type VariantProps } from "class-variance-authority"
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<
React.ElementRef<typeof SheetPrimitive.Overlay>,
@@ -20,13 +20,13 @@ const SheetOverlay = React.forwardRef<
<SheetPrimitive.Overlay
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",
className,
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
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",
@@ -44,64 +44,95 @@ const sheetVariants = cva(
defaultVariants: {
side: "right",
},
},
);
}
)
interface SheetContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> { }
const SheetContent = React.forwardRef<React.ElementRef<typeof SheetPrimitive.Content>, SheetContentProps>(
({ side = "right", className, children, ...props }, ref) => (
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ side }), className)}
{...props}
>
{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" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
),
);
SheetContent.displayName = SheetPrimitive.Content.displayName;
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetHeader = ({
className,
...props
}: 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>) => (
<div className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} {...props} />
);
SheetFooter.displayName = "SheetFooter";
const SheetFooter = ({
className,
...props
}: 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<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title ref={ref} className={cn("text-lg font-semibold text-foreground", className)} {...props} />
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description ref={ref} className={cn("text-sm text-muted-foreground", className)} {...props} />
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetOverlay,
SheetPortal,
SheetFooter,
SheetTitle,
SheetTrigger,
};
SheetDescription,
}

View File

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