calendar delete and bug fixes
This commit is contained in:
+77
-43
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useCalendarApp, ScheduleXCalendar } from '@schedule-x/react';
|
import { useCalendarApp, ScheduleXCalendar } from '@schedule-x/react';
|
||||||
import { createViewWeek } from '@schedule-x/calendar';
|
import { CalendarEventExternal, createViewWeek } from '@schedule-x/calendar';
|
||||||
|
|
||||||
import { createEventsServicePlugin } from '@schedule-x/events-service';
|
import { createEventsServicePlugin } from '@schedule-x/events-service';
|
||||||
import { createDragAndDropPlugin } from '@schedule-x/drag-and-drop';
|
import { createDragAndDropPlugin } from '@schedule-x/drag-and-drop';
|
||||||
@@ -15,74 +15,85 @@ import {
|
|||||||
ContextMenuTrigger,
|
ContextMenuTrigger,
|
||||||
} from '@/components/ui/context-menu';
|
} from '@/components/ui/context-menu';
|
||||||
|
|
||||||
import { PlusCircle } from 'lucide-react';
|
import { PlusCircle, Trash } from 'lucide-react';
|
||||||
|
|
||||||
import { parse } from 'date-fns';
|
import { format, parse } from 'date-fns';
|
||||||
import PostDialog, { today } from '@/components/postDialog';
|
import PostDialog, { today } from '@/components/postDialog';
|
||||||
|
|
||||||
|
function closestChild(children: any, clientX: number, clientY: number) {
|
||||||
|
let closestElement = null;
|
||||||
|
let closestDistance = Infinity;
|
||||||
|
|
||||||
|
for (let child of children) {
|
||||||
|
const childRect = child.getBoundingClientRect();
|
||||||
|
const dx = Math.abs(clientX - (childRect.left + childRect.width / 2));
|
||||||
|
const dy = Math.abs(clientY - (childRect.top + childRect.height / 2));
|
||||||
|
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||||
|
|
||||||
|
if (distance < closestDistance) {
|
||||||
|
closestDistance = distance;
|
||||||
|
closestElement = child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestElement;
|
||||||
|
}
|
||||||
|
|
||||||
function CalendarApp() {
|
function CalendarApp() {
|
||||||
const eventsService = useState(() => createEventsServicePlugin())[0];
|
const eventsService = useState(() => createEventsServicePlugin())[0];
|
||||||
const dragAndDrop = useState(() => createDragAndDropPlugin())[0];
|
const dragAndDrop = useState(() => createDragAndDropPlugin())[0];
|
||||||
|
const [eventCount, setEventCount] = useState(0);
|
||||||
|
|
||||||
const calendar = useCalendarApp({
|
const calendar = useCalendarApp({
|
||||||
views: [createViewWeek()],
|
views: [createViewWeek()],
|
||||||
events: [
|
events: [],
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
title: 'Linus tech tips drama',
|
|
||||||
start: '2025-02-16 01:00',
|
|
||||||
end: '2025-02-16 02:00',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
plugins: [eventsService, dragAndDrop],
|
plugins: [eventsService, dragAndDrop],
|
||||||
isDark: true,
|
isDark: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {}, [eventsService]);
|
|
||||||
|
|
||||||
const [newPostDialogOpen, setNewPostDialogOpen] = useState(false);
|
const [newPostDialogOpen, setNewPostDialogOpen] = useState(false);
|
||||||
const [date, setDate] = useState<Date>(today);
|
const [date, setDate] = useState<Date>(today);
|
||||||
|
const [selected, setSelected] = useState<string | null | undefined>(null);
|
||||||
|
|
||||||
const handleDialog: MouseEventHandler<HTMLSpanElement> = (event) => {
|
const handleDialog: MouseEventHandler<HTMLSpanElement> = (event) => {
|
||||||
const target = event.target as HTMLElement;
|
const target = event.target as HTMLElement;
|
||||||
|
|
||||||
let hourElement = null;
|
if (target.className === 'sx__time-grid-event-time') {
|
||||||
let closestDistance = Infinity;
|
setSelected(
|
||||||
const children = target.parentElement?.children[0].children || [];
|
target.parentElement?.parentElement?.getAttribute(
|
||||||
|
'data-event-id'
|
||||||
for (let child of children) {
|
)
|
||||||
const childRect = child.getBoundingClientRect();
|
|
||||||
const dx = Math.abs(
|
|
||||||
event.clientX - (childRect.left + childRect.width / 2)
|
|
||||||
);
|
);
|
||||||
const dy = Math.abs(
|
return;
|
||||||
event.clientY - (childRect.top + childRect.height / 2)
|
} else {
|
||||||
);
|
setSelected(null);
|
||||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
||||||
|
|
||||||
if (distance < closestDistance) {
|
|
||||||
closestDistance = distance;
|
|
||||||
hourElement = child;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dayElement = target.closest('.sx__time-grid-day');
|
try {
|
||||||
|
const dayElement = target.closest('.sx__time-grid-day');
|
||||||
|
|
||||||
const dayText = dayElement
|
const dayText = dayElement
|
||||||
? dayElement.getAttribute('aria-label')
|
? dayElement.getAttribute('aria-label')
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (!dayText || !hourElement) {
|
const hourElement = closestChild(
|
||||||
event.preventDefault();
|
target.parentElement?.children[0].children || [],
|
||||||
} else {
|
event.clientX,
|
||||||
setDate(
|
event.clientY
|
||||||
parse(
|
);
|
||||||
|
|
||||||
|
if (!dayText || !hourElement) {
|
||||||
|
event.preventDefault();
|
||||||
|
} else {
|
||||||
|
const toDate = parse(
|
||||||
(dayText || '') + ' ' + (hourElement?.textContent || ''),
|
(dayText || '') + ' ' + (hourElement?.textContent || ''),
|
||||||
'MMMM dd, yyyy h a',
|
'MMMM dd, yyyy h a',
|
||||||
new Date()
|
new Date()
|
||||||
)
|
);
|
||||||
);
|
if (toDate instanceof Date && !isNaN(toDate.getTime()))
|
||||||
}
|
setDate(toDate);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -93,6 +104,11 @@ function CalendarApp() {
|
|||||||
setDate={setDate}
|
setDate={setDate}
|
||||||
newPostDialogOpen={newPostDialogOpen}
|
newPostDialogOpen={newPostDialogOpen}
|
||||||
setNewPostDialogOpen={setNewPostDialogOpen}
|
setNewPostDialogOpen={setNewPostDialogOpen}
|
||||||
|
setEvents={(e) => {
|
||||||
|
calendar.events.add(e);
|
||||||
|
setEventCount(eventCount + 1);
|
||||||
|
}}
|
||||||
|
eventId={eventCount}
|
||||||
/>
|
/>
|
||||||
<ContextMenu>
|
<ContextMenu>
|
||||||
<ContextMenuTrigger onContextMenu={handleDialog}>
|
<ContextMenuTrigger onContextMenu={handleDialog}>
|
||||||
@@ -108,8 +124,26 @@ function CalendarApp() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusCircle className="w-4 mt-0.5" />{' '}
|
<PlusCircle className="w-4 mt-0.5" />{' '}
|
||||||
<p className="ml-1">New post</p>x
|
<p className="ml-1">New post</p>
|
||||||
</ContextMenuItem>
|
</ContextMenuItem>
|
||||||
|
|
||||||
|
{selected ? (
|
||||||
|
<ContextMenuItem
|
||||||
|
onClick={() => {
|
||||||
|
if (selected) {
|
||||||
|
calendar.events.remove(
|
||||||
|
parseInt(selected)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
<Trash className="w-4 mt-0.5 text-red-500" />{' '}
|
||||||
|
<p className="ml-1 text-red-500">Delete</p>
|
||||||
|
</ContextMenuItem>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</ContextMenuContent>
|
</ContextMenuContent>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+35
-24
@@ -1,8 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { Menu, ChevronDown, ChevronRight } from 'lucide-react';
|
import { Menu } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
@@ -10,12 +11,6 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import {
|
|
||||||
Collapsible,
|
|
||||||
CollapsibleContent,
|
|
||||||
CollapsibleTrigger,
|
|
||||||
} from '@/components/ui/collapsible';
|
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -57,6 +52,7 @@ const footer = [
|
|||||||
|
|
||||||
export function HamburgerMenu() {
|
export function HamburgerMenu() {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
@@ -71,7 +67,11 @@ export function HamburgerMenu() {
|
|||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<div key={i} className="w-16 mx-auto">
|
<div key={i} className="w-16 mx-auto">
|
||||||
<Link href={item.url}>
|
<Link href={item.url}>
|
||||||
<item.icon className="!size-full p-4 hover:bg-foreground hover:text-background rounded-2xl" />
|
{pathname === item.url ? (
|
||||||
|
<item.icon className="!size-full p-2 bg-foreground text-background rounded-2xl" />
|
||||||
|
) : (
|
||||||
|
<item.icon className="!size-full p-2 hover:bg-foreground hover:text-background rounded-2xl" />
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -81,7 +81,11 @@ export function HamburgerMenu() {
|
|||||||
<SheetFooter className="mt-auto">
|
<SheetFooter className="mt-auto">
|
||||||
{footer.map((item, i) => (
|
{footer.map((item, i) => (
|
||||||
<Link href={item.url} key={i}>
|
<Link href={item.url} key={i}>
|
||||||
<item.icon className="!size-full p-4 hover:bg-foreground hover:text-background rounded-2xl" />
|
{pathname === item.url ? (
|
||||||
|
<item.icon className="!size-full p-2 bg-foreground text-background rounded-2xl" />
|
||||||
|
) : (
|
||||||
|
<item.icon className="!size-full p-2 hover:bg-foreground hover:text-background rounded-2xl" />
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</SheetFooter>
|
</SheetFooter>
|
||||||
@@ -92,6 +96,7 @@ export function HamburgerMenu() {
|
|||||||
|
|
||||||
export default function App({ children }: { children?: any }) {
|
export default function App({ children }: { children?: any }) {
|
||||||
const isSmall = useSmallScreen();
|
const isSmall = useSmallScreen();
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
return isSmall ? (
|
return isSmall ? (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@@ -100,7 +105,11 @@ export default function App({ children }: { children?: any }) {
|
|||||||
{items.map((item, i) => (
|
{items.map((item, i) => (
|
||||||
<div key={i} className="w-12 mx-auto">
|
<div key={i} className="w-12 mx-auto">
|
||||||
<Link href={item.url}>
|
<Link href={item.url}>
|
||||||
<item.icon className="!size-full p-2 hover:bg-foreground hover:text-background rounded-2xl" />
|
{pathname === item.url ? (
|
||||||
|
<item.icon className="!size-full p-2 bg-foreground text-background rounded-2xl" />
|
||||||
|
) : (
|
||||||
|
<item.icon className="!size-full p-2 hover:bg-foreground hover:text-background rounded-2xl" />
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -110,28 +119,22 @@ export default function App({ children }: { children?: any }) {
|
|||||||
{footer.map((item, i) => (
|
{footer.map((item, i) => (
|
||||||
<div key={i} className="w-12 mx-auto">
|
<div key={i} className="w-12 mx-auto">
|
||||||
<Link href={item.url}>
|
<Link href={item.url}>
|
||||||
<item.icon className="!size-full p-2 hover:bg-foreground hover:text-background rounded-2xl" />
|
{pathname === item.url ? (
|
||||||
|
<item.icon className="!size-full p-2 bg-foreground text-background rounded-2xl" />
|
||||||
|
) : (
|
||||||
|
<item.icon className="!size-full p-2 hover:bg-foreground hover:text-background rounded-2xl" />
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{/* {items.map((item, i) => (
|
|
||||||
<div key={i} className="w-12 mx-auto">
|
|
||||||
<Link href={item.url}>
|
|
||||||
<item.icon className="!size-full p-2 hover:bg-foreground hover:text-background rounded-2xl" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
))} */}
|
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<div className="md:w-28">
|
<div className="md:w-28">
|
||||||
<SidebarProvider
|
<SidebarProvider open={true} defaultOpen={true}>
|
||||||
open={true}
|
|
||||||
defaultOpen={true}
|
|
||||||
>
|
|
||||||
<Sidebar className="w-28">
|
<Sidebar className="w-28">
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup>
|
<SidebarGroup>
|
||||||
@@ -146,7 +149,11 @@ export default function App({ children }: { children?: any }) {
|
|||||||
className="w-16 mx-auto"
|
className="w-16 mx-auto"
|
||||||
>
|
>
|
||||||
<Link href={item.url}>
|
<Link href={item.url}>
|
||||||
<item.icon className="!size-full p-4 hover:bg-foreground hover:text-background rounded-2xl" />
|
{pathname === item.url ? (
|
||||||
|
<item.icon className="!size-full p-4 bg-foreground text-background rounded-2xl" />
|
||||||
|
) : (
|
||||||
|
<item.icon className="!size-full p-4 hover:bg-foreground hover:text-background rounded-2xl" />
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
@@ -162,7 +169,11 @@ export default function App({ children }: { children?: any }) {
|
|||||||
className="w-16 mx-auto"
|
className="w-16 mx-auto"
|
||||||
>
|
>
|
||||||
<Link href={item.url}>
|
<Link href={item.url}>
|
||||||
<item.icon className="!size-full p-4 hover:bg-foreground hover:text-background rounded-2xl" />
|
{pathname === item.url ? (
|
||||||
|
<item.icon className="!size-full p-4 bg-foreground text-background rounded-2xl" />
|
||||||
|
) : (
|
||||||
|
<item.icon className="!size-full p-4 hover:bg-foreground hover:text-background rounded-2xl" />
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
@@ -17,12 +18,15 @@ import { format } from 'date-fns';
|
|||||||
import { CalendarIcon } from 'lucide-react';
|
import { CalendarIcon } from 'lucide-react';
|
||||||
import { Calendar } from './ui/calendar';
|
import { Calendar } from './ui/calendar';
|
||||||
import TimeSelect from './timeSelect';
|
import TimeSelect from './timeSelect';
|
||||||
|
import { CalendarEventExternal } from '@schedule-x/calendar';
|
||||||
|
|
||||||
type PostDialogProps = {
|
type PostDialogProps = {
|
||||||
newPostDialogOpen: boolean;
|
newPostDialogOpen: boolean;
|
||||||
setNewPostDialogOpen: (b: boolean) => void;
|
setNewPostDialogOpen: (b: boolean) => void;
|
||||||
date: Date;
|
date: Date;
|
||||||
setDate: (d: Date) => void;
|
setDate: (d: Date) => void;
|
||||||
|
eventId: number;
|
||||||
|
setEvents: (e: CalendarEventExternal) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const today = new Date();
|
export const today = new Date();
|
||||||
@@ -33,12 +37,15 @@ export default function PostDialog({
|
|||||||
setNewPostDialogOpen,
|
setNewPostDialogOpen,
|
||||||
date,
|
date,
|
||||||
setDate,
|
setDate,
|
||||||
|
setEvents,
|
||||||
|
eventId,
|
||||||
}: PostDialogProps) {
|
}: PostDialogProps) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={newPostDialogOpen} onOpenChange={setNewPostDialogOpen}>
|
<Dialog open={newPostDialogOpen} onOpenChange={setNewPostDialogOpen}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Feature not available</DialogTitle>
|
<DialogTitle>New post</DialogTitle>
|
||||||
|
<DialogDescription>Schedule posts on multiple accounts</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<Popover>
|
<Popover>
|
||||||
@@ -63,7 +70,7 @@ export default function PostDialog({
|
|||||||
selected={date}
|
selected={date}
|
||||||
onSelect={(d: Date | undefined) => {
|
onSelect={(d: Date | undefined) => {
|
||||||
if (d) {
|
if (d) {
|
||||||
const newDate = new Date(d); // Clone d
|
const newDate = new Date(d);
|
||||||
newDate.setHours(
|
newDate.setHours(
|
||||||
date.getHours(),
|
date.getHours(),
|
||||||
date.getMinutes(),
|
date.getMinutes(),
|
||||||
@@ -93,7 +100,24 @@ export default function PostDialog({
|
|||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button>Submit</Button>
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const end = new Date(date);
|
||||||
|
end.setHours(date.getHours() + 1);
|
||||||
|
|
||||||
|
setEvents({
|
||||||
|
title: 'My title',
|
||||||
|
id: eventId,
|
||||||
|
start: format(date, 'y-MM-dd hh:mm'),
|
||||||
|
end: format(end, 'y-MM-dd hh:mm'),
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
setNewPostDialogOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Schedule post
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -23,19 +23,18 @@ const TimeSelect: React.FC<TimeSelectProps> = ({
|
|||||||
setDate,
|
setDate,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
// Generate time options for all hours (00-23) with minutes (15, 30, 45)
|
|
||||||
const times = Array.from({ length: 24 }, (_, h) =>
|
const times = Array.from({ length: 24 }, (_, h) =>
|
||||||
[15, 30, 45].map((m) => `${h.toString().padStart(2, '0')}:${m}`)
|
[0, 15, 30, 45].map((m) => `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`)
|
||||||
).flat();
|
).flat();
|
||||||
|
|
||||||
const currentHour = date.getHours().toString().padStart(2, '0');
|
const currentHour = date.getHours().toString().padStart(2, '0');
|
||||||
const currentMinute = date.getMinutes();
|
const currentMinute = date.getMinutes();
|
||||||
const nearestMinute = [15, 30, 45].reduce((prev, curr) =>
|
const nearestMinute = [0, 15, 30, 45].reduce((prev, curr) =>
|
||||||
Math.abs(curr - currentMinute) < Math.abs(prev - currentMinute)
|
Math.abs(curr - currentMinute) < Math.abs(prev - currentMinute)
|
||||||
? curr
|
? curr
|
||||||
: prev
|
: prev
|
||||||
);
|
);
|
||||||
const selectedTime = `${currentHour}:${nearestMinute}`;
|
const selectedTime = `${currentHour}:${nearestMinute.toString().padStart(2, '0')}`;
|
||||||
|
|
||||||
const handleTimeChange = (newTime: string) => {
|
const handleTimeChange = (newTime: string) => {
|
||||||
const [hour, minute] = newTime.split(':').map(Number);
|
const [hour, minute] = newTime.split(':').map(Number);
|
||||||
|
|||||||
Reference in New Issue
Block a user