updated dialog and calendar

This commit is contained in:
2025-02-17 11:35:28 +01:00
parent 2740004a00
commit 74cf6889b8
8 changed files with 548 additions and 113 deletions
+58 -51
View File
@@ -1,17 +1,12 @@
'use client';
import { useCalendarApp, ScheduleXCalendar } from '@schedule-x/react';
import {
createViewDay,
createViewMonthAgenda,
createViewMonthGrid,
createViewWeek,
} from '@schedule-x/calendar';
import { createViewWeek } from '@schedule-x/calendar';
import { createEventsServicePlugin } from '@schedule-x/events-service';
import { createDragAndDropPlugin } from '@schedule-x/drag-and-drop';
import '@/calendar-theme.css';
import { useEffect, useState } from 'react';
import { MouseEventHandler, useEffect, useState } from 'react';
import App from '@/components/app';
import {
ContextMenu,
@@ -21,26 +16,16 @@ import {
} from '@/components/ui/context-menu';
import { PlusCircle } from 'lucide-react';
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { parse } from 'date-fns';
import PostDialog, { today } from '@/components/postDialog';
function CalendarApp() {
const eventsService = useState(() => createEventsServicePlugin())[0];
const dragAndDrop = useState(() => createDragAndDropPlugin())[0];
const calendar = useCalendarApp({
views: [
createViewDay(),
createViewWeek(),
createViewMonthGrid(),
createViewMonthAgenda(),
],
views: [createViewWeek()],
events: [
{
id: '1',
@@ -58,38 +43,64 @@ function CalendarApp() {
}, [eventsService]);
const [newPostDialogOpen, setNewPostDialogOpen] = useState(false);
const [date, setDate] = useState<Date>(today);
const handleDialog: MouseEventHandler<HTMLSpanElement> = (event) => {
const target = event.target as HTMLElement;
let hourElement = null;
let closestDistance = Infinity;
const children = target.parentElement?.children[0].children || [];
for (let child of children) {
const childRect = child.getBoundingClientRect();
const dx = Math.abs(
event.clientX - (childRect.left + childRect.width / 2)
);
const dy = Math.abs(
event.clientY - (childRect.top + childRect.height / 2)
);
const distance = Math.sqrt(dx * dx + dy * dy);
if (distance < closestDistance) {
closestDistance = distance;
hourElement = child;
}
}
const dayElement = target.closest('.sx__time-grid-day');
const dayText = dayElement
? dayElement.getAttribute('aria-label')
: null;
if (!dayText || !hourElement) {
event.preventDefault();
} else {
setDate(
parse(
(dayText || '') + ' ' + (hourElement?.textContent || ''),
'MMMM dd, yyyy h a',
new Date()
)
);
}
};
return (
<App>
<div>
<Dialog
open={newPostDialogOpen}
onOpenChange={setNewPostDialogOpen}
>
<DialogContent>
<DialogHeader>
<DialogTitle>Feature not available</DialogTitle>
</DialogHeader>
<div></div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setNewPostDialogOpen(false)}
>
Cancel
</Button>
<Button>Submit</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<PostDialog
date={date}
setDate={setDate}
newPostDialogOpen={newPostDialogOpen}
setNewPostDialogOpen={setNewPostDialogOpen}
/>
<ContextMenu>
<ContextMenuTrigger>
<ContextMenuTrigger onContextMenu={handleDialog}>
<ScheduleXCalendar
calendarApp={calendar}
customComponents={{
headerContent: CalendarHeader,
}}
customComponents={{}}
/>
</ContextMenuTrigger>
<ContextMenuContent>
@@ -99,7 +110,7 @@ function CalendarApp() {
}}
>
<PlusCircle className="w-4 mt-0.5" />{' '}
<p className="ml-1">New post</p>
<p className="ml-1">New post</p>x
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
@@ -108,8 +119,4 @@ function CalendarApp() {
);
}
function CalendarHeader() {
return <div className="flex flex-row w-full"></div>;
}
export default CalendarApp;
+39 -62
View File
@@ -14,66 +14,43 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
*/
:root {
--sx-color-primary: #6750a4;
--sx-color-on-primary: #fff;
--sx-color-primary-container: #eaddff;
--sx-color-on-primary-container: #21005e;
--sx-color-secondary: #625b71;
--sx-color-on-secondary: #fff;
--sx-color-secondary-container: #e8def8;
--sx-color-on-secondary-container: #1e192b;
--sx-color-tertiary: #7d5260;
--sx-color-on-tertiary: #fff;
--sx-color-tertiary-container: #ffd8e4;
--sx-color-on-tertiary-container: #370b1e;
--sx-color-surface: #fef7ff;
--sx-color-surface-dim: #ded8e1;
--sx-color-surface-bright: #fef7ff;
--sx-color-on-surface: #1c1b1f;
--sx-color-surface-container: #f3edf7;
--sx-color-surface-container-low: #f7f2fa;
--sx-color-surface-container-high: #ece6f0;
--sx-color-background: #fff;
--sx-color-on-background: #1c1b1f;
--sx-color-outline: #79747e;
--sx-color-outline-variant: #c4c7c5;
--sx-color-shadow: #000;
--sx-color-surface-tint: #6750a4;
--sx-color-neutral: var(--sx-color-outline);
--sx-color-neutral-variant: var(--sx-color-outline-variant);
--sx-internal-color-gray-ripple-background: #e0e0e0;
--sx-internal-color-light-gray: #fafafa;
--sx-internal-color-text: #000;
--sx-color-primary: hsl(var(--primary));
--sx-color-on-primary: hsl(var(--primary-foreground));
--sx-color-primary-container: hsl(var(--secondary));
--sx-color-on-primary-container: hsl(var(--secondary-foreground));
--sx-color-secondary: hsl(var(--muted));
--sx-color-on-secondary: hsl(var(--muted-foreground));
--sx-color-secondary-container: hsl(var(--accent));
--sx-color-on-secondary-container: hsl(var(--accent-foreground));
--sx-color-tertiary: hsl(var(--chart-1));
--sx-color-on-tertiary: hsl(var(--foreground));
--sx-color-tertiary-container: hsl(var(--chart-2));
--sx-color-on-tertiary-container: hsl(var(--foreground));
--sx-color-surface: hsl(var(--background));
--sx-color-surface-dim: hsl(var(--card));
--sx-color-surface-bright: hsl(var(--popover));
--sx-color-on-surface: hsl(var(--foreground));
--sx-color-surface-container: hsl(var(--card));
--sx-color-surface-container-low: hsl(var(--popover));
--sx-color-surface-container-high: hsl(var(--secondary));
--sx-color-on-background: hsl(var(--foreground));
--sx-color-background: hsl(var(--background));
--sx-color-outline: hsl(var(--border));
--sx-color-outline-variant: hsl(var(--input));
--sx-color-shadow: hsl(var(--background));
--sx-color-surface-tint: var(--sx-color-primary);
--sx-internal-color-text: hsl(var(--foreground));
}
.is-dark {
--sx-color-primary: #d0bcff;
--sx-color-on-primary: #371e73;
--sx-color-primary-container: #4f378b;
--sx-color-on-primary-container: #eaddff;
--sx-color-secondary: #ccc2dc;
--sx-color-on-secondary: #332d41;
--sx-color-secondary-container: #4a4458;
--sx-color-on-secondary-container: #e8def8;
--sx-color-tertiary: #efb8c8;
--sx-color-on-tertiary: #492532;
--sx-color-tertiary-container: #633b48;
--sx-color-on-tertiary-container: #ffd8e4;
--sx-color-surface: #141218;
--sx-color-surface-dim: #141218;
--sx-color-surface-bright: #3b383e;
--sx-color-on-surface: #e6e1e5;
--sx-color-surface-container: #211f26;
--sx-color-surface-container-low: #1d1b20;
--sx-color-surface-container-high: #2b2930;
--sx-color-background: #141218;
--sx-color-on-background: #e6e1e5;
--sx-color-outline: #938f99;
--sx-color-outline-variant: #444746;
--sx-color-shadow: #000;
--sx-color-surface-tint: #d0bcff;
--sx-internal-color-text: #fff;
}
/* .is-dark {} */
:root {
--sx-spacing-padding1: 4px;
@@ -90,7 +67,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
--sx-rounding-extra-small: 4px;
--sx-rounding-small: 8px;
--sx-rounding-extra-large: 28px;
--sx-border: 1px solid var(--sx-color-outline-variant);
/* --sx-border: 1px solid var(--sx-color-outline-variant); */
}
.is-dark {
@@ -497,7 +474,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
position: relative;
flex: 1;
height: 100%;
border: var(--sx-border);
/* border: var(--sx-border); */ /* Disabled this to make it look cleaner*/
border-radius: var(--sx-rounding-small);
display: flex;
flex-flow: column;
@@ -513,7 +490,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
}
.sx__slide-left {
animation: sx-slide-left 0.3s ease-out;
/* animation: sx-slide-left 0.3s ease-out; */
}
@keyframes sx-slide-left {
@@ -529,7 +506,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
}
}
.sx__slide-right {
animation: sx-slide-right 0.3s ease-out;
/* animation: sx-slide-right 0.3s ease-out; */
}
@keyframes sx-slide-right {
+101
View File
@@ -0,0 +1,101 @@
import {
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { format } from 'date-fns';
import { CalendarIcon } from 'lucide-react';
import { Calendar } from './ui/calendar';
import TimeSelect from './timeSelect';
type PostDialogProps = {
newPostDialogOpen: boolean;
setNewPostDialogOpen: (b: boolean) => void;
date: Date;
setDate: (d: Date) => void;
};
export const today = new Date();
today.setHours(0, 0, 0, 0);
export default function PostDialog({
newPostDialogOpen,
setNewPostDialogOpen,
date,
setDate,
}: PostDialogProps) {
return (
<Dialog open={newPostDialogOpen} onOpenChange={setNewPostDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Feature not available</DialogTitle>
</DialogHeader>
<div className="flex flex-row gap-4">
<Popover>
<PopoverTrigger asChild>
<Button
variant={'outline'}
className={cn(
'w-[280px] justify-start text-left font-normal',
!date && 'text-muted-foreground'
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? (
format(date, 'MMMM dd y')
) : (
<span>Pick a date</span>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
selected={date}
onSelect={(d: Date | undefined) => {
if (d) {
const newDate = new Date(d); // Clone d
newDate.setHours(
date.getHours(),
date.getMinutes(),
0,
0
);
setDate(newDate);
}
}}
mode="single"
disabled={(date) => date < today}
/>
</PopoverContent>
</Popover>
<TimeSelect
date={date || new Date()}
setDate={setDate}
className=""
/>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={() => setNewPostDialogOpen(false)}
>
Cancel
</Button>
<Button>Submit</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+68
View File
@@ -0,0 +1,68 @@
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
interface TimeSelectProps {
date: Date;
setDate: (newDate: Date) => void;
className?: string;
}
const TimeSelect: React.FC<TimeSelectProps> = ({
date,
setDate,
className,
}) => {
// Generate time options for all hours (00-23) with minutes (15, 30, 45)
const times = Array.from({ length: 24 }, (_, h) =>
[15, 30, 45].map((m) => `${h.toString().padStart(2, '0')}:${m}`)
).flat();
const currentHour = date.getHours().toString().padStart(2, '0');
const currentMinute = date.getMinutes();
const nearestMinute = [15, 30, 45].reduce((prev, curr) =>
Math.abs(curr - currentMinute) < Math.abs(prev - currentMinute)
? curr
: prev
);
const selectedTime = `${currentHour}:${nearestMinute}`;
const handleTimeChange = (newTime: string) => {
const [hour, minute] = newTime.split(':').map(Number);
const newDate = new Date(date);
newDate.setHours(hour, minute, 0, 0);
setDate(newDate);
};
return (
<div>
<Select onValueChange={handleTimeChange}>
<SelectTrigger>
<SelectValue
placeholder={selectedTime}
className={className}
/>
</SelectTrigger>
<SelectContent>
{times.map((t) => (
<SelectItem key={t} value={t}>
{t}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
);
};
export default TimeSelect;
+33
View File
@@ -0,0 +1,33 @@
"use client"
import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"
import { cn } from "@/lib/utils"
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
ref={ref}
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</PopoverPrimitive.Portal>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
+159
View File
@@ -0,0 +1,159 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}