Skip to content

Commit

Permalink
better datepicker
Browse files Browse the repository at this point in the history
  • Loading branch information
adityachoudhari26 committed Oct 31, 2024
1 parent d951178 commit 99f7ab4
Show file tree
Hide file tree
Showing 5 changed files with 979 additions and 150 deletions.
Original file line number Diff line number Diff line change
@@ -1,106 +1,64 @@
import type * as SCHEMA from "@ctrlplane/db/schema";
import { addDays, addHours, addMinutes, format } from "date-fns";
import { IconX } from "@tabler/icons-react";
import { z } from "zod";

import { Button } from "@ctrlplane/ui/button";
import { Checkbox } from "@ctrlplane/ui/checkbox";
import { DateTimePicker } from "@ctrlplane/ui/datetime-picker";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
useForm,
} from "@ctrlplane/ui/form";
import { Input } from "@ctrlplane/ui/input";
import { Label } from "@ctrlplane/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ctrlplane/ui/select";
import { Textarea } from "@ctrlplane/ui/textarea";

import { api } from "~/trpc/react";

const schema = z.object({
name: z.string().min(1).max(100),
description: z.string().max(1000).nullable(),
durationNumber: z.number().min(0).nullable(),
durationUnit: z.enum(["minutes", "hours", "days"]).nullable(),
removeExpiration: z.boolean(),
expiresAt: z
.date()
.min(new Date(), "Expires at must be in the future")
.optional(),
});

type OverviewProps = {
environment: SCHEMA.Environment;
};

const getExpiresAt = (
expiresAt: Date | null,
durationNumber: number,
durationUnit: "minutes" | "hours" | "days",
) => {
const currExpiresAt = expiresAt ?? new Date();
if (durationUnit === "minutes")
return addMinutes(currExpiresAt, durationNumber);
if (durationUnit === "hours") return addHours(currExpiresAt, durationNumber);
return addDays(currExpiresAt, durationNumber);
const isUsing12HourClock = (): boolean => {
const date = new Date();
const options: Intl.DateTimeFormatOptions = {
hour: "numeric",
};
const formattedTime = new Intl.DateTimeFormat(undefined, options).format(
date,
);
return formattedTime.includes("AM") || formattedTime.includes("PM");
};

export const Overview: React.FC<OverviewProps> = ({ environment }) => {
const defaultValues = {
...environment,
durationNumber: null,
durationUnit: "hours" as const,
removeExpiration: false,
};
const expiresAt = environment.expiresAt ?? undefined;
const defaultValues = { ...environment, expiresAt };
const form = useForm({ schema, defaultValues });
const update = api.environment.update.useMutation();
const envOverride = api.job.trigger.create.byEnvId.useMutation();

const utils = api.useUtils();

const { id, systemId } = environment;
const onSubmit = form.handleSubmit((data) => {
const { durationNumber, durationUnit, removeExpiration } = data;
const expiresAt = removeExpiration
? null
: durationNumber != null && durationUnit != null
? getExpiresAt(environment.expiresAt, durationNumber, durationUnit)
: environment.expiresAt;

const envData = { ...data, expiresAt };

const resetValues = {
...data,
durationNumber: null,
removeExpiration: false,
};
const onSubmit = form.handleSubmit((data) =>
update
.mutateAsync({ id, data: envData })
.then(() => form.reset(resetValues))
.mutateAsync({ id, data })
.then(() => form.reset(data))
.then(() => utils.environment.bySystemId.invalidate(systemId))
.then(() => utils.environment.byId.invalidate(id));
});

const currExpiresAt = environment.expiresAt;
const { durationNumber, durationUnit, removeExpiration } = form.watch();

const currentExpiration =
currExpiresAt != null
? format(currExpiresAt, "MMM d, yyyy h:mm a")
: "never";

const newExpiration = removeExpiration
? "never"
: durationNumber != null && durationUnit != null
? format(
getExpiresAt(currExpiresAt, durationNumber, durationUnit),
"MMM d, yyyy h:mm a",
)
: "no change";
.then(() => utils.environment.byId.invalidate(id)),
);

return (
<Form {...form}>
Expand Down Expand Up @@ -133,89 +91,35 @@ export const Overview: React.FC<OverviewProps> = ({ environment }) => {
</FormItem>
)}
/>

<div className="space-y-4">
<Label>Environment expiration</Label>
<div className="flex flex-col gap-1 text-sm text-muted-foreground">
<span>Current expiration: {currentExpiration}</span>
<span>New expiration: {newExpiration}</span>
</div>

<div className="flex items-center gap-2 text-sm">
{currExpiresAt == null && <span>Environment expires in: </span>}
{currExpiresAt != null && <span>Extend expiration by: </span>}
<FormField
control={form.control}
name="durationNumber"
render={({ field: { value, onChange } }) => (
<FormItem className="w-16">
<FormControl>
<Input
type="number"
value={value ?? ""}
className="appearance-none [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
onChange={(e) => {
const num = e.target.valueAsNumber;
if (Number.isNaN(num)) {
onChange(null);
return;
}
onChange(num);
}}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="durationUnit"
render={({ field: { value, onChange } }) => (
<FormItem className="w-24">
<FormControl>
<Select
value={value ?? undefined}
onValueChange={onChange}
defaultValue="hours"
>
<SelectTrigger>
<SelectValue placeholder="Select duration unit..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="minutes">Minutes</SelectItem>
<SelectItem value="hours">Hours</SelectItem>
<SelectItem value="days">Days</SelectItem>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
</div>

<FormField
control={form.control}
name="removeExpiration"
render={({ field: { value, onChange } }) => (
<FormItem>
<FormControl>
<div className="flex items-center gap-2">
<Checkbox
checked={value}
onCheckedChange={(v) => {
onChange(v);
if (v) form.setValue("durationNumber", null);
}}
/>
<label htmlFor="removeExpiration" className="text-sm">
Remove expiration
</label>
</div>
</FormControl>
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="expiresAt"
render={({ field: { value, onChange } }) => (
<FormItem>
<FormLabel>Expires at</FormLabel>
<FormControl>
<div className="flex items-center gap-2">
<DateTimePicker
value={value}
onChange={onChange}
granularity="minute"
hourCycle={isUsing12HourClock() ? 12 : 24}
className="w-60"
/>
<Button
variant="ghost"
size="icon"
type="button"
onClick={() => onChange(undefined)}
>
<IconX className="h-4 w-4" />
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<div className="flex items-center gap-2">
<Button
Expand Down
4 changes: 3 additions & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,18 @@
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-switch": "^1.1.0",
"@radix-ui/react-tabs": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.1",
"@tabler/icons-react": "^3.17.0",
"class-variance-authority": "^0.7.0",
"cmdk": "^1.0.0",
"date-fns": "^4.1.0",
"lucide-react": "^0.441.0",
"next-themes": "^0.3.0",
"react-aria": "^3.33.1",
"react-day-picker": "^9.2.1",
"react-hook-form": "^7.51.4",
"react-resizable-panels": "^2.0.20",
"react-stately": "^3.31.1",
Expand Down
73 changes: 73 additions & 0 deletions packages/ui/src/calendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"use client";

import * as React from "react";
import { ChevronLeftIcon } from "@radix-ui/react-icons";
import { DayPicker } from "react-day-picker";

import { buttonVariants } from "./button";
import { cn } from "./index";

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

function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", 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",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100",
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: cn(
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
props.mode === "range"
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
: "[&:has([aria-selected])]:rounded-md",
),
day: cn(
buttonVariants({ variant: "ghost" }),
"h-8 w-8 p-0 font-normal aria-selected:opacity-100",
),
day_range_start: "day-range-start",
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",
day_today: "bg-accent text-accent-foreground",
day_outside:
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
Chevron: ({ ...props }) => (
<ChevronLeftIcon className="h-4 w-4" {...props} />
),
}}
{...props}
/>
);
}
Calendar.displayName = "Calendar";

export { Calendar };
Loading

0 comments on commit 99f7ab4

Please sign in to comment.