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:
@@ -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 */}
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user