mirror of
https://github.com/colanode/colanode.git
synced 2025-12-16 11:47:47 +01:00
208 lines
6.8 KiB
TypeScript
208 lines
6.8 KiB
TypeScript
import { ChevronDown, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
import { useState } from 'react';
|
|
import { DayPicker, DayProps, getDefaultClassNames } from 'react-day-picker';
|
|
|
|
import {
|
|
FieldAttributes,
|
|
isSameDay,
|
|
DatabaseViewFilterAttributes,
|
|
} from '@colanode/core';
|
|
import { CalendarViewDay } from '@colanode/ui/components/databases/calendars/calendar-view-day';
|
|
import { buttonVariants } from '@colanode/ui/components/ui/button';
|
|
import { useDatabase } from '@colanode/ui/contexts/database';
|
|
import { useDatabaseView } from '@colanode/ui/contexts/database-view';
|
|
import { useWorkspace } from '@colanode/ui/contexts/workspace';
|
|
import { useRecordsQuery } from '@colanode/ui/hooks/use-records-query';
|
|
import { filterRecords } from '@colanode/ui/lib/databases';
|
|
import { cn, getDisplayedDates } from '@colanode/ui/lib/utils';
|
|
|
|
const toUTCDate = (dateParam: Date | string): Date => {
|
|
const date = typeof dateParam === 'string' ? new Date(dateParam) : dateParam;
|
|
|
|
const year = date.getFullYear();
|
|
const month = date.getMonth();
|
|
const day = date.getDate();
|
|
|
|
return new Date(Date.UTC(year, month, day, 0, 0, 0, 0));
|
|
};
|
|
|
|
interface CalendarViewGridProps {
|
|
field: FieldAttributes;
|
|
}
|
|
|
|
export const CalendarViewGrid = ({ field }: CalendarViewGridProps) => {
|
|
const workspace = useWorkspace();
|
|
const database = useDatabase();
|
|
const view = useDatabaseView();
|
|
|
|
const defaultClassNames = getDefaultClassNames();
|
|
|
|
const [month, setMonth] = useState(new Date());
|
|
const { first, last } = getDisplayedDates(month);
|
|
|
|
const filters: DatabaseViewFilterAttributes[] = [
|
|
...view.filters,
|
|
{
|
|
id: 'start_date',
|
|
type: 'field',
|
|
fieldId: field.id,
|
|
operator: 'is_on_or_after',
|
|
value: first.toISOString(),
|
|
},
|
|
{
|
|
id: 'end_date',
|
|
type: 'field',
|
|
fieldId: field.id,
|
|
operator: 'is_on_or_before',
|
|
value: last.toISOString(),
|
|
},
|
|
];
|
|
|
|
const { records } = useRecordsQuery(filters, view.sorts, 200);
|
|
|
|
return (
|
|
<DayPicker
|
|
showOutsideDays
|
|
className="p-3"
|
|
month={month}
|
|
onMonthChange={(month) => {
|
|
setMonth(month);
|
|
}}
|
|
formatters={{
|
|
formatMonthDropdown: (date) =>
|
|
date.toLocaleString('default', { month: 'short' }),
|
|
}}
|
|
classNames={{
|
|
root: cn('w-full', defaultClassNames.root),
|
|
months: cn(
|
|
'flex gap-4 flex-col md:flex-row relative w-full',
|
|
defaultClassNames.months
|
|
),
|
|
month: cn('flex flex-col w-full gap-4', defaultClassNames.month),
|
|
nav: cn(
|
|
'flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between',
|
|
defaultClassNames.nav
|
|
),
|
|
button_previous: cn(
|
|
buttonVariants({ variant: 'ghost' }),
|
|
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none size-7',
|
|
defaultClassNames.button_previous
|
|
),
|
|
button_next: cn(
|
|
buttonVariants({ variant: 'ghost' }),
|
|
'size-(--cell-size) aria-disabled:opacity-50 p-0 select-none size-7',
|
|
defaultClassNames.button_next
|
|
),
|
|
month_caption: cn(
|
|
'flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)',
|
|
defaultClassNames.month_caption
|
|
),
|
|
dropdowns: cn(
|
|
'w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5',
|
|
defaultClassNames.dropdowns
|
|
),
|
|
dropdown_root: cn(
|
|
'relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md',
|
|
defaultClassNames.dropdown_root
|
|
),
|
|
dropdown: cn('absolute inset-0 opacity-0', defaultClassNames.dropdown),
|
|
caption_label: cn(
|
|
'select-none font-medium',
|
|
'rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5',
|
|
defaultClassNames.caption_label
|
|
),
|
|
table: 'w-full border-collapse',
|
|
weekdays: cn('flex', defaultClassNames.weekdays),
|
|
weekday: cn(
|
|
'text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none',
|
|
defaultClassNames.weekday
|
|
),
|
|
week: cn(
|
|
'flex w-full mt-2 border-b first:border-t border-border',
|
|
defaultClassNames.week
|
|
),
|
|
week_number_header: cn(
|
|
'select-none w-(--cell-size)',
|
|
defaultClassNames.week_number_header
|
|
),
|
|
week_number: cn(
|
|
'text-[0.8rem] select-none text-muted-foreground',
|
|
defaultClassNames.week_number
|
|
),
|
|
range_start: cn(
|
|
'rounded-l-md bg-accent',
|
|
defaultClassNames.range_start
|
|
),
|
|
range_middle: cn('rounded-none', defaultClassNames.range_middle),
|
|
range_end: cn('rounded-r-md bg-accent', defaultClassNames.range_end),
|
|
today: cn(
|
|
'bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none',
|
|
defaultClassNames.today
|
|
),
|
|
outside: cn(
|
|
'text-muted-foreground aria-selected:text-muted-foreground',
|
|
defaultClassNames.outside
|
|
),
|
|
disabled: cn(
|
|
'text-muted-foreground opacity-50',
|
|
defaultClassNames.disabled
|
|
),
|
|
hidden: cn('invisible', defaultClassNames.hidden),
|
|
}}
|
|
components={{
|
|
Chevron: ({ className, orientation, ...props }) => {
|
|
if (orientation === 'left') {
|
|
return (
|
|
<ChevronLeft className={cn('size-4', className)} {...props} />
|
|
);
|
|
}
|
|
|
|
if (orientation === 'right') {
|
|
return (
|
|
<ChevronRight className={cn('size-4', className)} {...props} />
|
|
);
|
|
}
|
|
|
|
return <ChevronDown className={cn('size-4', className)} {...props} />;
|
|
},
|
|
Day: (props: DayProps) => {
|
|
const day = toUTCDate(props.day.date);
|
|
const filter: DatabaseViewFilterAttributes = {
|
|
id: 'calendar_filter',
|
|
type: 'field',
|
|
fieldId: field.id,
|
|
operator: 'is_equal_to',
|
|
value: day.toISOString(),
|
|
};
|
|
|
|
const dayRecords = filterRecords(
|
|
records,
|
|
filter,
|
|
field,
|
|
workspace.userId
|
|
);
|
|
|
|
const canCreate =
|
|
(field.type === 'created_at' &&
|
|
isSameDay(props.day.date, new Date())) ||
|
|
field.type === 'date';
|
|
|
|
const onCreate =
|
|
database.canCreateRecord && canCreate
|
|
? () => view.createRecord([filter])
|
|
: undefined;
|
|
|
|
return (
|
|
<CalendarViewDay
|
|
date={props.day.date}
|
|
records={dayRecords}
|
|
onCreate={onCreate}
|
|
isOutside={props.day.outside}
|
|
/>
|
|
);
|
|
},
|
|
}}
|
|
/>
|
|
);
|
|
};
|