Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"next-themes": "^0.4.6",
"nuqs": "^2.4.1",
"react": "^19.0.0",
"react-day-picker": "^9.9.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.56.3",
"react-markdown": "^10.1.0",
Expand Down
106 changes: 106 additions & 0 deletions apps/web-v2/src/components/ui/accept-schedule-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import * as React from "react";
import { ChevronDown, Calendar as CalendarIcon } from "lucide-react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { DateTimePicker } from "@/components/ui/date-time-picker";

interface AcceptScheduleButtonProps {
onAccept: () => void;
onSchedule: (scheduledDate: Date) => void;
acceptDisabled?: boolean;
scheduleDisabled?: boolean;
className?: string;
acceptText?: string;
scheduleText?: string;
}

export function AcceptScheduleButton({
onAccept,
onSchedule,
acceptDisabled = false,
scheduleDisabled = false,
className,
acceptText = "Accept",
scheduleText = "Schedule",
}: AcceptScheduleButtonProps) {
const [scheduledDate, setScheduledDate] = React.useState<Date>();
const [isScheduleOpen, setIsScheduleOpen] = React.useState(false);

const handleScheduleConfirm = () => {
if (scheduledDate) {
onSchedule(scheduledDate);
setIsScheduleOpen(false);
setScheduledDate(undefined);
}
};

return (
<div className={cn("flex items-center", className)}>
{/* Accept Button */}
<Button
onClick={onAccept}
disabled={acceptDisabled}
className="rounded-r-none border-r-0 bg-[#2F6868] font-medium text-white hover:bg-[#2F6868]/90"
size="sm"
>
{acceptText}
</Button>

{/* Schedule Dropdown */}
<Popover
open={isScheduleOpen}
onOpenChange={setIsScheduleOpen}
>
<PopoverTrigger asChild>
<Button
variant="outline"
disabled={scheduleDisabled}
className="rounded-l-none border-l-0 bg-white px-3 font-medium text-gray-700 hover:bg-gray-50 border-gray-300"
size="sm"
>
<span className="mr-1">{scheduleText}</span>
<ChevronDown className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0"
align="end"
>
<div className="space-y-4 p-4">
<DateTimePicker
date={scheduledDate}
setDate={setScheduledDate}
placeholder="Select date and time"
/>
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => {
setIsScheduleOpen(false);
setScheduledDate(undefined);
}}
>
Cancel
</Button>
<Button
size="sm"
onClick={handleScheduleConfirm}
disabled={!scheduledDate}
className="bg-[#2F6868] text-white hover:bg-[#2F6868]/90"
>
{scheduleText}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
</div>
);
}
64 changes: 64 additions & 0 deletions apps/web-v2/src/components/ui/calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as React from "react";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { DayPicker } from "react-day-picker";

import { cn } from "@/lib/utils";
import { buttonVariants } from "@/components/ui/button";

export type CalendarProps = React.ComponentProps<typeof DayPicker>;

function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-4 bg-card rounded-lg border shadow-sm", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center mb-4",
caption_label: "text-lg font-semibold text-foreground",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-8 w-8 bg-transparent p-0 opacity-70 hover:opacity-100 hover:bg-accent hover:text-accent-foreground transition-all duration-200",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex mb-2",
head_cell:
"text-muted-foreground rounded-md w-10 h-10 font-medium text-sm flex items-center justify-center",
row: "flex w-full mt-2",
cell: "h-10 w-10 text-center text-sm p-0 relative [&:has([aria-selected].day-range-end)]:rounded-r-md [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-10 w-10 p-0 font-normal aria-selected:opacity-100 rounded-md hover:bg-accent hover:text-accent-foreground transition-all duration-200",
),
day_range_end: "day-range-end",
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground shadow-sm",
day_today: "bg-accent text-accent-foreground font-semibold ring-2 ring-primary/20",
day_outside:
"day-outside text-muted-foreground opacity-40 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-20 cursor-not-allowed hover:bg-transparent hover:text-muted-foreground",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";

export { Calendar };
165 changes: 165 additions & 0 deletions apps/web-v2/src/components/ui/date-time-picker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import * as React from "react";
import { format } from "date-fns";
import { Calendar as CalendarIcon, Clock } from "lucide-react";

import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";

interface DateTimePickerProps {
date?: Date;
setDate: (date: Date | undefined) => void;
placeholder?: string;
className?: string;
}

export function DateTimePicker({
date,
setDate,
placeholder = "Pick a date and time",
className,
}: DateTimePickerProps) {
const [time, setTime] = React.useState<string>("10:30");
const [isOpen, setIsOpen] = React.useState(false);

const handleDateSelect = (selectedDate: Date | undefined) => {
if (selectedDate) {
const [hours, minutes] = time.split(":");
const newDate = new Date(selectedDate);
newDate.setHours(parseInt(hours, 10));
newDate.setMinutes(parseInt(minutes, 10));
setDate(newDate);
} else {
setDate(undefined);
}
};

const handleTimeChange = (newTime: string) => {
setTime(newTime);
if (date) {
const [hours, minutes] = newTime.split(":");
const newDate = new Date(date);
newDate.setHours(parseInt(hours, 10));
newDate.setMinutes(parseInt(minutes, 10));

// Check if the selected datetime is in the past
const now = new Date();
if (newDate <= now) {
// If in the past, don't update the date state yet
// The validation will happen in handleConfirm
return;
}

setDate(newDate);
}
};

const handleConfirm = () => {
if (date) {
const [hours, minutes] = time.split(":");
const newDate = new Date(date);
newDate.setHours(parseInt(hours, 10));
newDate.setMinutes(parseInt(minutes, 10));

// Check if the selected datetime is in the past
const now = new Date();
if (newDate <= now) {
// If in the past, set to current time + 1 minute minimum
const minimumTime = new Date(now.getTime() + 60000); // Add 1 minute
setDate(minimumTime);

// Update the time input to reflect the adjusted time
const adjustedHours = minimumTime.getHours().toString().padStart(2, "0");
const adjustedMinutes = minimumTime.getMinutes().toString().padStart(2, "0");
setTime(`${adjustedHours}:${adjustedMinutes}`);
} else {
setDate(newDate);
}
}
setIsOpen(false);
};

React.useEffect(() => {
if (date) {
const hours = date.getHours().toString().padStart(2, "0");
const minutes = date.getMinutes().toString().padStart(2, "0");
setTime(`${hours}:${minutes}`);
}
}, [date]);

return (
<Popover
open={isOpen}
onOpenChange={setIsOpen}
>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-[300px] justify-start text-left font-normal h-11 px-4 bg-background hover:bg-accent hover:text-accent-foreground border-input shadow-sm transition-all duration-200",
!date && "text-muted-foreground",
className,
)}
>
<CalendarIcon className="mr-3 h-4 w-4 text-muted-foreground" />
{date ? (
<span className="font-medium">{format(date, "PPP 'at' p")}</span>
) : (
<span className="text-muted-foreground">{placeholder}</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0 shadow-lg border-0"
align="start"
>
<div className="flex bg-card rounded-lg overflow-hidden">
<div className="min-w-[320px]">
<Calendar
mode="single"
selected={date}
onSelect={handleDateSelect}
initialFocus
disabled={(date) => date < new Date().setHours(0, 0, 0, 0)}
className="border-0 shadow-none"
/>
</div>
<div className="flex flex-col gap-4 border-l bg-muted/30 px-6 py-6 min-w-[160px]">
<div className="space-y-3">
<Label
htmlFor="time"
className="text-sm font-semibold text-foreground"
>
Select Time
</Label>
<div className="flex items-center gap-3 p-3 bg-background rounded-md border shadow-sm">
<Clock className="text-muted-foreground h-4 w-4 flex-shrink-0" />
<Input
id="time"
type="time"
value={time}
onChange={(e) => handleTimeChange(e.target.value)}
className="w-full border-0 bg-transparent p-0 text-sm font-medium focus-visible:ring-0"
/>
</div>
</div>
<Button
size="sm"
onClick={handleConfirm}
className="mt-2 w-full bg-primary hover:bg-primary/90 text-primary-foreground shadow-sm transition-all duration-200"
>
Confirm
</Button>
</div>
</div>
</PopoverContent>
</Popover>
);
}
Loading
Loading