Skip to content

Add calendar event management #2257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createClient } from '@tuturuuu/supabase/next/server';
import { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
import { NextResponse } from 'next/server';

interface Params {
params: Promise<{
wsId: string;
eventId: string;
}>;
}

export async function GET(_: Request, { params }: Params) {
const supabase = await createClient();
const { wsId, eventId } = await params;

try {
const { data: event, error } = await supabase
.from('workspace_calendar_events')
.select('*')
.eq('id', eventId)
.eq('ws_id', wsId)
.single();

if (error) throw error;

return NextResponse.json(event);
} catch (error) {
console.error('Calendar event API error:', error);
return NextResponse.json(
{ error: 'An error occurred while processing your request' },
{ status: 500 }
);
}
}

export async function PUT(request: Request, { params }: Params) {
const supabase = await createClient();
const { wsId, eventId } = await params;

try {
const updates: Partial<CalendarEvent> = await request.json();

const { data, error } = await supabase
.from('workspace_calendar_events')
.update({
title: updates.title,
description: updates.description,
start_at: updates.start_at,
end_at: updates.end_at,
color: updates.color,
})
.eq('id', eventId)
.eq('ws_id', wsId)
.select()
.single();

if (error) throw error;

return NextResponse.json(data);
} catch (error) {
console.error('Calendar event API error:', error);
return NextResponse.json(
{ error: 'An error occurred while processing your request' },
{ status: 500 }
);
}
}

export async function DELETE(_: Request, { params }: Params) {
const supabase = await createClient();
const { wsId, eventId } = await params;

try {
const { error } = await supabase
.from('workspace_calendar_events')
.delete()
.eq('id', eventId)
.eq('ws_id', wsId);

if (error) throw error;

return NextResponse.json({ message: 'Event deleted successfully' });
} catch (error) {
console.error('Calendar event API error:', error);
return NextResponse.json(
{ error: 'An error occurred while processing your request' },
{ status: 500 }
);
}
}
77 changes: 77 additions & 0 deletions apps/web/src/app/api/v1/workspaces/[wsId]/calendar/events/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { createClient } from '@tuturuuu/supabase/next/server';
import { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
import { NextResponse } from 'next/server';

interface Params {
params: Promise<{
wsId: string;
}>;
}

export async function GET(request: Request, { params }: Params) {
const supabase = await createClient();
const { wsId } = await params;

// Get the start_at and end_at from the URL
const url = new URL(request.url);
const start_at = url.searchParams.get('start_at');
const end_at = url.searchParams.get('end_at');

if (!start_at || !end_at) {
return NextResponse.json(
{ error: 'Start and end dates are required' },
{ status: 400 }
);
}

try {
const { data: events, error } = await supabase
.from('workspace_calendar_events')
.select('*')
.eq('ws_id', wsId)
.gte('start_at', new Date(start_at).toISOString())
.lte('end_at', new Date(end_at).toISOString());

if (error) throw error;

return NextResponse.json({ data: events, count: events.length });
} catch (error) {
console.error('Calendar events API error:', error);
return NextResponse.json(
{ error: 'An error occurred while processing your request' },
{ status: 500 }
);
}
}

export async function POST(request: Request, { params }: Params) {
const supabase = await createClient();
const { wsId } = await params;

try {
const event: Omit<CalendarEvent, 'id'> = await request.json();

const { data, error } = await supabase
.from('workspace_calendar_events')
.insert({
title: event.title || '',
description: event.description || '',
start_at: event.start_at,
end_at: event.end_at,
color: event.color || 'blue',
ws_id: wsId,
})
.select()
.single();

if (error) throw error;

return NextResponse.json(data, { status: 201 });
} catch (error) {
console.error('Calendar events API error:', error);
return NextResponse.json(
{ error: 'An error occurred while processing your request' },
{ status: 500 }
);
}
}
47 changes: 43 additions & 4 deletions apps/web/src/components/calendar/Calendar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,42 @@

import CalendarHeader from './CalendarHeader';
import CalendarViewWithTrail from './CalendarViewWithTrail';
import DynamicIsland from './DynamicIsland';
import { EventModal } from './EventModal';
import MonthCalendar from './MonthCalendar';
import WeekdayBar from './WeekdayBar';
import { CalendarProvider } from '@/hooks/useCalendar';
import { CalendarProvider, useCalendar } from '@/hooks/useCalendar';
import { CalendarView, useViewTransition } from '@/hooks/useViewTransition';
import { Workspace } from '@tuturuuu/types/primitives/Workspace';
import { Button } from '@tuturuuu/ui/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@tuturuuu/ui/tooltip';
import { cn } from '@tuturuuu/utils/format';
import { PlusIcon } from 'lucide-react';
import { useTranslations } from 'next-intl';
import { useCallback, useEffect, useState } from 'react';

// Floating action button for quick event creation
const CreateEventButton = () => {
const { openModal } = useCalendar();

return (
<div className="fixed right-6 bottom-6 z-10">
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
className="h-14 w-14 rounded-full shadow-lg"
onClick={() => openModal()}
>
<PlusIcon className="h-6 w-6" />
<span className="sr-only">Create new event</span>
</Button>
</TooltipTrigger>
<TooltipContent>Create new event</TooltipContent>
</Tooltip>
</div>
);
};

const Calendar = ({ workspace }: { workspace: Workspace }) => {
const t = useTranslations('calendar');
const { transition } = useViewTransition();
Expand Down Expand Up @@ -208,7 +235,14 @@ const Calendar = ({ workspace }: { workspace: Workspace }) => {

return (
<CalendarProvider ws={workspace}>
<div className="grid h-[calc(100%-4rem)] w-full md:pb-4">
<div
className={cn(
'grid h-[calc(100%-4rem)] w-full md:pb-4',
view === 'month'
? 'grid-rows-[auto_1fr]'
: 'grid-rows-[auto_auto_1fr]'
)}
>
<CalendarHeader
availableViews={availableViews}
date={date}
Expand All @@ -224,6 +258,7 @@ const Calendar = ({ workspace }: { workspace: Workspace }) => {
else if (newView === 'month') enableMonthView();
}}
/>

{view !== 'month' && <WeekdayBar view={view} dates={dates} />}

<div className="relative flex-1 overflow-hidden">
Expand All @@ -234,7 +269,11 @@ const Calendar = ({ workspace }: { workspace: Workspace }) => {
)}
</div>

<DynamicIsland />
{/* Event Creation/Editing Modal */}
<EventModal />

{/* Floating action button */}
<CreateEventButton />
</div>
</CalendarProvider>
);
Expand Down
48 changes: 30 additions & 18 deletions apps/web/src/components/calendar/CalendarCell.tsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,54 @@
// import { useCalendar } from '@/hooks/useCalendar';
import { useCalendar } from '@/hooks/useCalendar';
import { cn } from '@tuturuuu/utils/format';

interface CalendarCellProps {
date: string;
hour: number;
}

const CalendarCell = ({ date, hour }: CalendarCellProps) => {
// const { addEmptyEvent } = useCalendar();
const { addEmptyEvent } = useCalendar();

const id = `cell-${date}-${hour}`;

// const handleCreateEvent = (midHour?: boolean) => {
// const newDate = new Date(date);

// newDate.setDate(newDate.getDate() + 1);
// newDate.setHours(hour, midHour ? 30 : 0, 0, 0);

// addEmptyEvent(newDate);
// };
const handleCreateEvent = (midHour?: boolean) => {
const newDate = new Date(date);
newDate.setHours(hour, midHour ? 30 : 0, 0, 0);
addEmptyEvent(newDate);
};

return (
<div
id={id}
className={`calendar-cell grid h-20 border-r border-border dark:border-zinc-800 ${
className={cn(
'calendar-cell relative grid h-20 border-r transition-colors hover:bg-muted/10 dark:border-zinc-800',
hour !== 0 && 'border-t'
}`}
)}
onContextMenu={(e) => {
e.preventDefault();
}}
data-hour={hour}
data-date={date}
>
<button
className="row-span-2 cursor-default"
// onClick={() => handleCreateEvent()}
/>
className="group relative z-10 row-span-2 cursor-pointer focus:outline-none"
onClick={() => handleCreateEvent()}
title={`Create event at ${hour}:00`}
>
<span className="absolute top-0 left-2 text-xs font-medium text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100">
{hour < 10 ? `0${hour}:00` : `${hour}:00`}
</span>
</button>

<button
className="cursor-default"
// onClick={() => handleCreateEvent(true)}
/>
className="group cursor-pointer focus:outline-none"
onClick={() => handleCreateEvent(true)}
title={`Create event at ${hour}:30`}
>
<span className="absolute top-10 left-2 text-xs font-medium text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100">
{hour < 10 ? `0${hour}:30` : `${hour}:30`}
</span>
</button>
</div>
);
};
Expand Down
34 changes: 30 additions & 4 deletions apps/web/src/components/calendar/CalendarEventMatrix.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,46 @@ import { useParams } from 'next/navigation';
const CalendarEventMatrix = ({ dates }: { dates: Date[] }) => {
const params = useParams();
const wsId = params?.wsId as string;

const { getEvents } = useCalendar();

const events = getEvents();
// Get all events and filter them based on the visible dates
const allEvents = getEvents();
const visibleEvents = allEvents.filter(event => {
const eventStart = new Date(event.start_at);
const eventEnd = new Date(event.end_at);

// Check if the event falls within any of the visible dates
return dates.some(date => {
const isAllDay = eventStart.getHours() === 0 && eventEnd.getHours() === 23;

if (isAllDay) {
// For all-day events, check if the date falls within the event's range
return (
eventStart <= date &&
eventEnd >= date
);
}

// For regular events, check if the event starts on this date
return (
date.getFullYear() === eventStart.getFullYear() &&
date.getMonth() === eventStart.getMonth() &&
date.getDate() === eventStart.getDate()
);
});
});

const columns = dates.length;

return (
<div
className={`pointer-events-none absolute inset-0 grid ${
columns === 1 && 'max-w-lg'
}`}
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
>
<div id="calendar-event-matrix" className="relative">
{events.map((event) => (
<div id="calendar-event-matrix" className="relative col-span-full">
{visibleEvents.map((event) => (
<EventCard wsId={wsId} key={event.id} event={event} dates={dates} />
))}
</div>
Expand Down
Loading