Skip to content

Commit

Permalink
feat: add date components
Browse files Browse the repository at this point in the history
  • Loading branch information
walt-it committed Oct 4, 2024
1 parent 67c0c69 commit ff0dd34
Show file tree
Hide file tree
Showing 10 changed files with 204 additions and 16 deletions.
47 changes: 38 additions & 9 deletions src/domains/guest/screens/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,32 @@ import type { SubmitHandler } from "react-hook-form";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { currentTimezone } from "~/shared";
import { Button, Calendar, DatePicker } from "~/shared/components";
import {
Button,
Calendar,
DateField,
DatePicker,
RangeCalendar,
} from "~/shared/components";

const dateRules = z
.string({ message: "Date is required." })
.refine((value) => !isNaN(Date.parse(value)), {
message: "Invalid date string, must be a valid ISO 8601 format",
});

const dateRangeRules = z
.array(z.string())
.length(2, { message: "Exactly two dates are required." })
.refine((dates) => dates.every((date) => !isNaN(Date.parse(date))), {
message: "Each date must be a valid ISO 8601 string.",
});

const playgroundFormSchema = z.object({
date: dateRules,
datefield: dateRules,
datepicker: dateRules,
calendar: dateRules,
rangecalendar: dateRangeRules,
});

type PlaygroundForm = z.infer<typeof playgroundFormSchema>;
Expand All @@ -28,8 +42,10 @@ export function Playground() {
formState: { errors },
} = useForm<PlaygroundForm>({
defaultValues: {
date: undefined,
datefield: undefined,
datepicker: undefined,
calendar: undefined,
rangecalendar: undefined,
},
resolver: zodResolver(playgroundFormSchema),
});
Expand All @@ -39,18 +55,24 @@ export function Playground() {
const onSubmit: SubmitHandler<PlaygroundForm> = (form) => {
console.log({
...form,
tz: currentTimezone,
});
};

return (
<form onSubmit={handleSubmit(onSubmit)} className='flex flex-col space-y-3'>
<DateField
name='datefield'
label='Choose date from datefield'
control={control}
errorMessage={errors.datefield?.message}
description='This is a help text'
/>
<DatePicker
name='date'
name='datepicker'
control={control}
label='Choose date from datepicker'
errorMessage={errors.date?.message}
description='Pick a date'
errorMessage={errors.datepicker?.message}
description='This is a help text'
isDateUnavailable={(dateValue) =>
!isWeekend(dateValue, currentLocale.locale)
}
Expand All @@ -61,11 +83,18 @@ export function Playground() {
control={control}
label='Choose date from calendar'
errorMessage={errors.calendar?.message}
description='Pick a date'
description='This is a help text'
isDateUnavailable={(dateValue) =>
isWeekend(dateValue, currentLocale.locale)
}
/>
<RangeCalendar
name='rangecalendar'
control={control}
label='Choose range from calendar'
errorMessage={errors.rangecalendar?.message}
description='This is a help text'
/>
<Button type='submit' className='self-start'>
Submit
</Button>
Expand Down
5 changes: 2 additions & 3 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
font-smooth: never;
}

/* [data-testid="underlay"] {
[data-testid="underlay"] {
background: rgba(0, 0, 0, 0.4);
z-index: 10;
backdrop-filter: blur(2px);
} */
}
3 changes: 2 additions & 1 deletion src/shared/components/ui/Calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useController } from "react-hook-form";
import type { Control, FieldValues, Path } from "react-hook-form";
import { tv } from "tailwind-variants";

import { currentTimezone } from "~/utils";
import { CalendarHeader } from "./CalendarHeader";
import { CalendarTable } from "./CalendarTable";

Expand Down Expand Up @@ -46,7 +47,7 @@ export const Calendar = <T extends DateValue, U extends FieldValues>({
{...props}
aria-labelledby={label}
onChange={(newDate) => {
field.onChange(newDate.toString());
field.onChange(newDate.toDate(currentTimezone).toISOString());
}}
onFocusChange={field.onBlur}
// TODO: ref={field.ref} Need to pass this down to CalendarTable to focus on error
Expand Down
5 changes: 3 additions & 2 deletions src/shared/components/ui/Calendar/CalendarTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
CalendarGrid,
CalendarGridBody,
CalendarGridHeader,
CalendarGridProps,
CalendarHeaderCell,
} from "react-aria-components";
import { tv } from "tailwind-variants";
Expand All @@ -18,9 +19,9 @@ const calendarTable = tv({

const { root, dayOfWeek } = calendarTable();

export function CalendarTable() {
export function CalendarTable(props: CalendarGridProps) {
return (
<CalendarGrid weekdayStyle='short' className={root()}>
<CalendarGrid weekdayStyle='short' className={root()} {...props}>
<CalendarGridHeader>
{(day) => (
<CalendarHeaderCell className={dayOfWeek()}>{day}</CalendarHeaderCell>
Expand Down
74 changes: 74 additions & 0 deletions src/shared/components/ui/DateField/DateField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type {
DateFieldProps as AriaDateFieldProps,
DateValue,
} from "react-aria-components";
import {
DateField as AriaDateField,
DateInput,
DateSegment,
Label,
Text,
} from "react-aria-components";
import { Control, FieldValues, Path, useController } from "react-hook-form";
import { tv } from "tailwind-variants";

import { currentTimezone } from "~/utils";

const datefield = tv({
slots: {
dateInput:
"flex w-full items-center space-x-1 rounded-lg border p-2 md:w-fit",
dateSegment: "font-medium text-gray-800",
error: "text-xs text-red-500",
desc: "text-xs",
},
});

const { dateInput, dateSegment, error, desc } = datefield();

interface DateFieldProps<T extends DateValue, U extends FieldValues>
extends AriaDateFieldProps<T> {
name: Path<U>;
control: Control<U>;
label?: string;
description?: string;
errorMessage?: string;
}

export function DateField<T extends DateValue, U extends FieldValues>({
name,
control,
label,
description,
errorMessage,
...props
}: DateFieldProps<T, U>) {
const { field } = useController({ name, control });
return (
<AriaDateField
{...props}
onChange={(newDate) => {
field.onChange(newDate.toDate(currentTimezone).toISOString());
}}
onBlur={field.onBlur}
ref={field.ref}
>
<Label>{label}</Label>
<DateInput className={dateInput()}>
{(segment) => (
<DateSegment className={dateSegment()} segment={segment} />
)}
</DateInput>
{errorMessage && (
<Text className={error()} slot='errorMessage'>
{errorMessage}
</Text>
)}
{description && !errorMessage && (
<Text className={desc()} slot='description'>
{description}
</Text>
)}
</AriaDateField>
);
}
1 change: 1 addition & 0 deletions src/shared/components/ui/DateField/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./DateField";
3 changes: 2 additions & 1 deletion src/shared/components/ui/DatePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type { Control, FieldValues, Path } from "react-hook-form";
import { useController } from "react-hook-form";
import { tv } from "tailwind-variants";

import { currentTimezone } from "~/utils";
import { CalendarHeader } from "../Calendar/CalendarHeader";
import { CalendarTable } from "../Calendar/CalendarTable";

Expand Down Expand Up @@ -59,7 +60,7 @@ export const DatePicker = <T extends DateValue, U extends FieldValues>({
<AriaDatePicker
{...props}
onChange={(newDate) => {
field.onChange(newDate.toString());
field.onChange(newDate.toDate(currentTimezone).toISOString());
}}
onBlur={field.onBlur}
>
Expand Down
79 changes: 79 additions & 0 deletions src/shared/components/ui/RangeCalendar/RangeCalendar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type {
RangeCalendarProps as AriaRangeCalendarProps,
DateValue,
} from "react-aria-components";
import {
RangeCalendar as AriaRangeCalendar,
Label,
Text,
} from "react-aria-components";
import { Control, FieldValues, Path, useController } from "react-hook-form";
import { tv } from "tailwind-variants";

import { currentTimezone } from "~/utils";
import { CalendarHeader } from "../Calendar/CalendarHeader";
import { CalendarTable } from "../Calendar/CalendarTable";

const rangecalendar = tv({
slots: {
container: "w-full rounded-lg border bg-white p-5 md:w-fit",
error: "text-xs text-red-500",
desc: "text-xs",
},
});

const { container, error, desc } = rangecalendar();

interface RangeCalendarProps<T extends DateValue, U extends FieldValues>
extends AriaRangeCalendarProps<T> {
name: Path<U>;
control: Control<U>;
errorMessage?: string;
label?: string;
description?: string;
}

export function RangeCalendar<T extends DateValue, U extends FieldValues>({
errorMessage,
description,
label,
name,
control,
...props
}: RangeCalendarProps<T, U>) {
const { field } = useController({ name, control });
return (
<div>
{label && <Label id={label}>{label}</Label>}
<AriaRangeCalendar
{...props}
visibleDuration={{ months: 2 }}
aria-labelledby={label}
onChange={(newDate) => {
field.onChange([
newDate?.start.toDate(currentTimezone).toISOString(),
newDate?.end.toDate(currentTimezone).toISOString(),
]);
}}
onFocusChange={field.onBlur}
className={container()}
>
<CalendarHeader />
<div className='flex items-center space-x-6'>
<CalendarTable />
<CalendarTable offset={{ months: 1 }} />
</div>
</AriaRangeCalendar>
{errorMessage && (
<Text className={error()} slot='errorMessage'>
{errorMessage}
</Text>
)}
{description && !errorMessage && (
<Text className={desc()} slot='description'>
{description}
</Text>
)}
</div>
);
}
1 change: 1 addition & 0 deletions src/shared/components/ui/RangeCalendar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./RangeCalendar";
2 changes: 2 additions & 0 deletions src/shared/components/ui/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export * from "./Button";
export * from "./Calendar";
export * from "./DateField";
export * from "./DatePicker";
export * from "./IconWrapper";
export * from "./Modal";
export * from "./RangeCalendar";

0 comments on commit ff0dd34

Please sign in to comment.