updates to calendar items to use family auth context

This commit is contained in:
John Overton
2025-06-09 13:25:41 -05:00
parent e310d947ae
commit aaae32f684
7 changed files with 311 additions and 418 deletions

View File

@@ -4,7 +4,6 @@ import { ApiResponse } from '../types';
import { CalendarEventType, RecurrencePattern } from '@prisma/client';
import { withAuthContext, AuthResult } from '../utils/auth';
import { toUTC, formatForResponse } from '../utils/timezone';
import { getFamilyIdFromRequest } from '../utils/family';
// Type for calendar event response
interface CalendarEventResponse {
@@ -46,6 +45,7 @@ interface CalendarEventResponse {
address: string | null;
notes: string | null;
}>;
contactIds: string[];
}
// Type for calendar event create/update
@@ -71,6 +71,11 @@ interface CalendarEventCreate {
async function handleGet(req: NextRequest, authContext: AuthResult) {
try {
const { familyId: userFamilyId } = authContext;
if (!userFamilyId) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'User is not associated with a family.' }, { status: 403 });
}
const { searchParams } = new URL(req.url);
const id = searchParams.get('id');
const babyId = searchParams.get('babyId');
@@ -81,19 +86,12 @@ async function handleGet(req: NextRequest, authContext: AuthResult) {
const typeParam = searchParams.get('type');
const recurringParam = searchParams.get('recurring');
// Get family ID from request
const familyId = await getFamilyIdFromRequest(req);
// Build where clause
const where: any = {
deletedAt: null,
familyId: userFamilyId,
};
// Add family filter if available
if (familyId) {
where.familyId = familyId;
}
// Add filters
if (id) {
where.id = id;
@@ -140,8 +138,8 @@ async function handleGet(req: NextRequest, authContext: AuthResult) {
// If ID is provided, fetch a single event
if (id) {
const event = await prisma.calendarEvent.findUnique({
where: { id },
const event = await prisma.calendarEvent.findFirst({
where: { id, familyId: userFamilyId },
include: {
babies: {
include: {
@@ -177,18 +175,7 @@ async function handleGet(req: NextRequest, authContext: AuthResult) {
return NextResponse.json<ApiResponse<CalendarEventResponse>>(
{
success: false,
error: 'Calendar event not found',
},
{ status: 404 }
);
}
// Check family access
if (familyId && event.familyId !== familyId) {
return NextResponse.json<ApiResponse<CalendarEventResponse>>(
{
success: false,
error: 'Calendar event not found',
error: 'Calendar event not found or access denied',
},
{ status: 404 }
);
@@ -206,6 +193,7 @@ async function handleGet(req: NextRequest, authContext: AuthResult) {
babies: event.babies.map(be => be.baby),
caretakers: event.caretakers.map(ce => ce.caretaker),
contacts: event.contacts.map(ce => ce.contact),
contactIds: event.contacts.map(ce => ce.contact.id),
};
return NextResponse.json<ApiResponse<CalendarEventResponse>>({
@@ -263,6 +251,7 @@ async function handleGet(req: NextRequest, authContext: AuthResult) {
babies: event.babies.map(be => be.baby),
caretakers: event.caretakers.map(ce => ce.caretaker),
contacts: event.contacts.map(ce => ce.contact),
contactIds: event.contacts.map(ce => ce.contact.id),
}));
return NextResponse.json<ApiResponse<CalendarEventResponse[]>>({
@@ -283,26 +272,59 @@ async function handleGet(req: NextRequest, authContext: AuthResult) {
async function handlePost(req: NextRequest, authContext: AuthResult) {
try {
const { familyId: userFamilyId, caretakerId: userCaretakerId } = authContext;
if (!userFamilyId) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'User is not associated with a family.' }, { status: 403 });
}
const body: CalendarEventCreate = await req.json();
// Validate required fields
if (!body.title || !body.startTime || body.type === undefined || body.allDay === undefined) {
return NextResponse.json<ApiResponse<CalendarEventResponse>>(
{
success: false,
error: 'Missing required fields',
},
{ status: 400 }
);
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'Missing required fields' }, { status: 400 });
}
// Validate that all associated entities belong to the user's family
if (body.babyIds.length > 0) {
const babiesCount = await prisma.baby.count({
where: {
id: { in: body.babyIds },
familyId: userFamilyId,
},
});
if (babiesCount !== body.babyIds.length) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'One or more babies not found in this family.' }, { status: 404 });
}
}
if (body.caretakerIds.length > 0) {
const caretakersCount = await prisma.caretaker.count({
where: {
id: { in: body.caretakerIds },
familyId: userFamilyId,
},
});
if (caretakersCount !== body.caretakerIds.length) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'One or more caretakers not found in this family.' }, { status: 404 });
}
}
if (body.contactIds.length > 0) {
const contactsCount = await prisma.contact.count({
where: {
id: { in: body.contactIds },
familyId: userFamilyId,
},
});
if (contactsCount !== body.contactIds.length) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'One or more contacts not found in this family.' }, { status: 404 });
}
}
// Get family ID from request (with fallback to body)
const familyId = await getFamilyIdFromRequest(req) || body.familyId;
// Convert dates to UTC for storage
const startTimeUTC = toUTC(body.startTime);
const endTimeUTC = body.endTime ? toUTC(body.endTime) : undefined;
const recurrenceEndUTC = body.recurrenceEnd ? toUTC(body.recurrenceEnd) : undefined;
const endTimeUTC = body.endTime ? toUTC(body.endTime) : null;
const recurrenceEndUTC = body.recurrenceEnd ? toUTC(body.recurrenceEnd) : null;
// Create event
const event = await prisma.calendarEvent.create({
@@ -310,7 +332,7 @@ async function handlePost(req: NextRequest, authContext: AuthResult) {
title: body.title,
description: body.description || null,
startTime: startTimeUTC,
endTime: endTimeUTC || null,
endTime: endTimeUTC,
allDay: body.allDay,
type: body.type,
location: body.location || null,
@@ -321,23 +343,17 @@ async function handlePost(req: NextRequest, authContext: AuthResult) {
customRecurrence: body.customRecurrence || null,
reminderTime: body.reminderTime || null,
notificationSent: false,
familyId: familyId || null,
familyId: userFamilyId || null,
// Create relationships
babies: {
create: (body.babyIds || []).map(babyId => ({
baby: { connect: { id: babyId } },
})),
create: body.babyIds.map(babyId => ({ babyId })),
},
caretakers: {
create: (body.caretakerIds || []).map(caretakerId => ({
caretaker: { connect: { id: caretakerId } },
})),
create: body.caretakerIds.map(caretakerId => ({ caretakerId })),
},
contacts: {
create: (body.contactIds || []).map(contactId => ({
contact: { connect: { id: contactId } },
})),
create: body.contactIds.map(contactId => ({ contactId })),
},
},
include: {
@@ -383,6 +399,7 @@ async function handlePost(req: NextRequest, authContext: AuthResult) {
babies: event.babies.map(be => be.baby),
caretakers: event.caretakers.map(ce => ce.caretaker),
contacts: event.contacts.map(ce => ce.contact),
contactIds: event.contacts.map(ce => ce.contact.id),
};
return NextResponse.json<ApiResponse<CalendarEventResponse>>({
@@ -391,7 +408,7 @@ async function handlePost(req: NextRequest, authContext: AuthResult) {
}, { status: 201 });
} catch (error) {
console.error('Error creating calendar event:', error);
return NextResponse.json<ApiResponse<CalendarEventResponse>>(
return NextResponse.json<ApiResponse<null>>(
{
success: false,
error: 'Failed to create calendar event',
@@ -403,51 +420,57 @@ async function handlePost(req: NextRequest, authContext: AuthResult) {
async function handlePut(req: NextRequest, authContext: AuthResult) {
try {
const { searchParams } = new URL(req.url);
const id = searchParams.get('id');
const body: CalendarEventCreate = await req.json();
if (!id) {
return NextResponse.json<ApiResponse<CalendarEventResponse>>(
{
success: false,
error: 'Calendar event ID is required',
},
{ status: 400 }
);
const { familyId: userFamilyId } = authContext;
if (!userFamilyId) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'User is not associated with a family.' }, { status: 403 });
}
// Get family ID from request (with fallback to body)
const familyId = await getFamilyIdFromRequest(req) || body.familyId;
const { searchParams } = new URL(req.url);
const id = searchParams.get('id');
const body: Partial<CalendarEventCreate> = await req.json();
// Check if event exists and belongs to the family
const existingEvent = await prisma.calendarEvent.findUnique({
where: { id },
if (!id) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'Calendar event ID is required' }, { status: 400 });
}
const existingEvent = await prisma.calendarEvent.findFirst({
where: { id, familyId: userFamilyId },
});
if (!existingEvent || existingEvent.deletedAt) {
return NextResponse.json<ApiResponse<CalendarEventResponse>>(
{
success: false,
error: 'Calendar event not found',
},
{ status: 404 }
);
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'Calendar event not found or access denied' }, { status: 404 });
}
// Check family access
if (familyId && existingEvent.familyId !== familyId) {
return NextResponse.json<ApiResponse<CalendarEventResponse>>(
{
success: false,
error: 'Calendar event not found',
},
{ status: 404 }
);
// Validate associated entities
if (body.babyIds && body.babyIds.length > 0) {
const babiesCount = await prisma.baby.count({
where: { id: { in: body.babyIds }, familyId: userFamilyId },
});
if (babiesCount !== body.babyIds.length) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'One or more babies not found in this family.' }, { status: 404 });
}
}
if (body.caretakerIds && body.caretakerIds.length > 0) {
const caretakersCount = await prisma.caretaker.count({
where: { id: { in: body.caretakerIds }, familyId: userFamilyId },
});
if (caretakersCount !== body.caretakerIds.length) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'One or more caretakers not found in this family.' }, { status: 404 });
}
}
if (body.contactIds && body.contactIds.length > 0) {
const contactsCount = await prisma.contact.count({
where: { id: { in: body.contactIds }, familyId: userFamilyId },
});
if (contactsCount !== body.contactIds.length) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'One or more contacts not found in this family.' }, { status: 404 });
}
}
// Convert dates to UTC for storage
const startTimeUTC = toUTC(body.startTime);
const startTimeUTC = body.startTime ? toUTC(body.startTime) : undefined;
const endTimeUTC = body.endTime ? toUTC(body.endTime) : undefined;
const recurrenceEndUTC = body.recurrenceEnd ? toUTC(body.recurrenceEnd) : undefined;
@@ -475,24 +498,21 @@ async function handlePut(req: NextRequest, authContext: AuthResult) {
recurrenceEnd: recurrenceEndUTC || null,
customRecurrence: body.customRecurrence || null,
reminderTime: body.reminderTime || null,
familyId: familyId || existingEvent.familyId, // Preserve existing familyId if not provided
familyId: userFamilyId || existingEvent.familyId, // Preserve existing familyId if not provided
// Create new relationships
babies: {
create: (body.babyIds || []).map(babyId => ({
baby: { connect: { id: babyId } },
})),
},
caretakers: {
create: (body.caretakerIds || []).map(caretakerId => ({
caretaker: { connect: { id: caretakerId } },
})),
},
contacts: {
create: (body.contactIds || []).map(contactId => ({
contact: { connect: { id: contactId } },
})),
},
babies: body.babyIds ? {
deleteMany: {},
create: body.babyIds.map(babyId => ({ babyId })),
} : undefined,
caretakers: body.caretakerIds ? {
deleteMany: {},
create: body.caretakerIds.map(caretakerId => ({ caretakerId })),
} : undefined,
contacts: body.contactIds ? {
deleteMany: {},
create: body.contactIds.map(contactId => ({ contactId })),
} : undefined,
},
include: {
babies: {
@@ -540,6 +560,7 @@ async function handlePut(req: NextRequest, authContext: AuthResult) {
babies: updatedEvent.babies.map(be => be.baby),
caretakers: updatedEvent.caretakers.map(ce => ce.caretaker),
contacts: updatedEvent.contacts.map(ce => ce.contact),
contactIds: updatedEvent.contacts.map(ce => ce.contact.id),
};
return NextResponse.json<ApiResponse<CalendarEventResponse>>({
@@ -548,7 +569,7 @@ async function handlePut(req: NextRequest, authContext: AuthResult) {
});
} catch (error) {
console.error('Error updating calendar event:', error);
return NextResponse.json<ApiResponse<CalendarEventResponse>>(
return NextResponse.json<ApiResponse<null>>(
{
success: false,
error: 'Failed to update calendar event',
@@ -560,50 +581,25 @@ async function handlePut(req: NextRequest, authContext: AuthResult) {
async function handleDelete(req: NextRequest, authContext: AuthResult) {
try {
const { familyId: userFamilyId } = authContext;
if (!userFamilyId) {
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'User is not associated with a family.' }, { status: 403 });
}
const { searchParams } = new URL(req.url);
const id = searchParams.get('id');
if (!id) {
return NextResponse.json<ApiResponse<void>>(
{
success: false,
error: 'Calendar event ID is required',
},
{ status: 400 }
);
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'Calendar event ID is required' }, { status: 400 });
}
// Get family ID from request
const familyId = await getFamilyIdFromRequest(req);
// Check if event exists
const existingEvent = await prisma.calendarEvent.findUnique({
where: { id },
const existingEvent = await prisma.calendarEvent.findFirst({
where: { id, familyId: userFamilyId },
});
if (!existingEvent) {
return NextResponse.json<ApiResponse<void>>(
{
success: false,
error: 'Calendar event not found',
},
{ status: 404 }
);
return NextResponse.json<ApiResponse<null>>({ success: false, error: 'Calendar event not found or access denied' }, { status: 404 });
}
// Check family access
if (familyId && existingEvent.familyId !== familyId) {
return NextResponse.json<ApiResponse<void>>(
{
success: false,
error: 'Calendar event not found',
},
{ status: 404 }
);
}
// Allow deleting even if it's already marked as deleted
// This prevents errors when trying to delete an event multiple times
// Soft delete the event
await prisma.calendarEvent.update({
@@ -613,12 +609,10 @@ async function handleDelete(req: NextRequest, authContext: AuthResult) {
},
});
return NextResponse.json<ApiResponse<void>>({
success: true,
});
return NextResponse.json<ApiResponse<null>>({ success: true });
} catch (error) {
console.error('Error deleting calendar event:', error);
return NextResponse.json<ApiResponse<void>>(
return NextResponse.json<ApiResponse<null>>(
{
success: false,
error: 'Failed to delete calendar event',

View File

@@ -121,7 +121,7 @@ This checklist tracks the progress of refactoring each component and its associa
- API: `app/api/sleep-log/`
### Other Components
- [ ] **Calendar**
- [x] **Calendar**
- Form: `src/components/forms/CalendarEventForm/`
- API: `app/api/calendar-event/`
- [x] **Contacts**

View File

@@ -20,22 +20,11 @@ A responsive calendar component for the Baby Tracker application that displays a
import { Calendar } from '@/src/components/Calendar';
function CalendarPage() {
const { selectedBaby } = useBaby();
const { userTimezone } = useTimezone();
return (
<div className="h-full">
{selectedBaby ? (
<Calendar
selectedBabyId={selectedBaby.id}
userTimezone={userTimezone}
/>
) : (
<div className="text-center py-12">
<h2 className="text-2xl font-semibold">No Baby Selected</h2>
<p className="mt-2 text-gray-500">Please select a baby from the dropdown menu above.</p>
</div>
)}
<Calendar userTimezone={userTimezone} />
</div>
);
}
@@ -51,7 +40,7 @@ Main component for displaying a monthly calendar with activity indicators.
| Prop | Type | Description | Default |
|------|------|-------------|---------|
| `selectedBabyId` | `string \| undefined` | The ID of the currently selected baby | Required |
| `onDateSelect` | `(date: Date) => void` | Optional callback when a date is selected | `undefined` |
| `userTimezone` | `string` | The user's timezone for date calculations | Required |
## Visual Behavior
@@ -63,6 +52,7 @@ Main component for displaying a monthly calendar with activity indicators.
- Month and year are displayed in the header
- Navigation buttons allow moving to previous/next months
- "Today" button returns to the current month
- The component will handle sending this timezone to the API
## Implementation Details

View File

@@ -101,8 +101,6 @@ export function Calendar({ selectedBabyId, userTimezone, onDateSelect }: Calenda
* Fetch events for the selected month
*/
const fetchEvents = async () => {
if (!selectedBabyId) return;
try {
// Create start date (first day of month) and end date (last day of month)
const year = currentDate.getFullYear();
@@ -115,11 +113,10 @@ export function Calendar({ selectedBabyId, userTimezone, onDateSelect }: Calenda
endDate.setHours(23, 59, 59, 999);
console.log(`Fetching events for date range: ${startDate.toISOString()} to ${endDate.toISOString()}`);
console.log(`Baby ID: ${selectedBabyId}, Timezone: ${userTimezone}`);
// Fetch calendar events
const eventsResponse = await fetch(
`/api/calendar-event?babyId=${selectedBabyId}&startDate=${startDate.toISOString()}&endDate=${endDate.toISOString()}&timezone=${encodeURIComponent(userTimezone)}`
`/api/calendar-event?startDate=${startDate.toISOString()}&endDate=${endDate.toISOString()}`
);
const eventsData = await eventsResponse.json();
@@ -166,7 +163,7 @@ export function Calendar({ selectedBabyId, userTimezone, onDateSelect }: Calenda
*/
useEffect(() => {
fetchEvents();
}, [selectedBabyId, currentDate, userTimezone]);
}, [currentDate, userTimezone]);
// Removed fetchEventsForSelectedDay and its useEffect hook

View File

@@ -14,7 +14,6 @@ import {
import CalendarEventForm from '@/src/components/forms/CalendarEventForm';
import { CalendarEventFormData } from '@/src/components/forms/CalendarEventForm/calendar-event-form.types';
import './calendar-day-view.css';
import { useFamily } from '@/src/context/family';
/**
* CalendarDayView Component
@@ -41,7 +40,6 @@ export const CalendarDayView: React.FC<CalendarDayViewProps> = ({
onClose,
isOpen,
}) => {
const { family } = useFamily();
// State for event form
const [showEventForm, setShowEventForm] = useState(false);
@@ -54,22 +52,16 @@ export const CalendarDayView: React.FC<CalendarDayViewProps> = ({
React.useEffect(() => {
const fetchData = async () => {
try {
// Build URLs with family ID for proper data filtering
const urlParams = new URLSearchParams();
if (family?.id) {
urlParams.append('familyId', family.id);
}
// Fetch babies
const babiesResponse = await fetch(`/api/baby?${urlParams.toString()}`);
const babiesResponse = await fetch('/api/baby');
const babiesData = await babiesResponse.json();
// Fetch caretakers
const caretakersResponse = await fetch(`/api/caretaker?${urlParams.toString()}`);
const caretakersResponse = await fetch('/api/caretaker');
const caretakersData = await caretakersResponse.json();
// Fetch contacts
const contactsResponse = await fetch(`/api/contact?${urlParams.toString()}`);
const contactsResponse = await fetch('/api/contact');
const contactsData = await contactsResponse.json();
// Update state with fetched data
@@ -84,7 +76,7 @@ export const CalendarDayView: React.FC<CalendarDayViewProps> = ({
if (isOpen) {
fetchData();
}
}, [isOpen, family?.id]);
}, [isOpen]);
// Format date for display
const formattedDate = useMemo(() => {
@@ -250,26 +242,14 @@ export const CalendarDayView: React.FC<CalendarDayViewProps> = ({
// Handle event delete
const handleDeleteEvent = async (eventId: string) => {
if (!eventId) return;
try {
const response = await fetch(`/api/calendar-event?id=${eventId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ familyId: family?.id }),
});
const data = await response.json();
if (data.success) {
// Close form
setShowEventForm(false);
// Notify parent component to refresh
if (onAddEvent) {
onAddEvent(date);
onAddEvent(date); // Trigger refresh
}
} else {
console.error('Error deleting event:', data.error);
@@ -304,81 +284,83 @@ export const CalendarDayView: React.FC<CalendarDayViewProps> = ({
// Events state - grouped by time of day
return (
<div className="calendar-day-view">
{/* Morning events */}
{groupedEvents.morning.length > 0 && (
<div className={styles.eventGroup}>
<div className={styles.eventGroupHeader}>
<Sun className={styles.eventGroupIcon} />
<h3 className={cn(
styles.eventGroupTitle,
'calendar-day-view-group-title'
)}>
Morning
</h3>
<div className="calendar-day-view px-3">
<div className="max-w-2xl mx-auto mt-2">
{/* Morning events */}
{groupedEvents.morning.length > 0 && (
<div className={styles.eventGroup}>
<div className={styles.eventGroupHeader}>
<Sun className={styles.eventGroupIcon} />
<h3 className={cn(
styles.eventGroupTitle,
'calendar-day-view-group-title'
)}>
Morning
</h3>
</div>
<div className={styles.eventsList}>
{groupedEvents.morning.map(event => (
<CalendarEventItem
key={event.id}
event={event}
onClick={handleEventClick}
/>
))}
</div>
</div>
<div className={styles.eventsList}>
{groupedEvents.morning.map(event => (
<CalendarEventItem
key={event.id}
event={event}
onClick={handleEventClick}
/>
))}
)}
{/* Afternoon events */}
{groupedEvents.afternoon.length > 0 && (
<div className={styles.eventGroup}>
<div className={styles.eventGroupHeader}>
<Coffee className={styles.eventGroupIcon} />
<h3 className={cn(
styles.eventGroupTitle,
'calendar-day-view-group-title'
)}>
Afternoon
</h3>
</div>
<div className={styles.eventsList}>
{groupedEvents.afternoon.map(event => (
<CalendarEventItem
key={event.id}
event={event}
onClick={handleEventClick}
/>
))}
</div>
</div>
</div>
)}
{/* Afternoon events */}
{groupedEvents.afternoon.length > 0 && (
<div className={styles.eventGroup}>
<div className={styles.eventGroupHeader}>
<Coffee className={styles.eventGroupIcon} />
<h3 className={cn(
styles.eventGroupTitle,
'calendar-day-view-group-title'
)}>
Afternoon
</h3>
)}
{/* Evening events */}
{groupedEvents.evening.length > 0 && (
<div className={styles.eventGroup}>
<div className={styles.eventGroupHeader}>
<Moon className={styles.eventGroupIcon} />
<h3 className={cn(
styles.eventGroupTitle,
'calendar-day-view-group-title'
)}>
Evening
</h3>
</div>
<div className={styles.eventsList}>
{groupedEvents.evening.map(event => (
<CalendarEventItem
key={event.id}
event={event}
onClick={handleEventClick}
/>
))}
</div>
</div>
<div className={styles.eventsList}>
{groupedEvents.afternoon.map(event => (
<CalendarEventItem
key={event.id}
event={event}
onClick={handleEventClick}
/>
))}
</div>
</div>
)}
{/* Evening events */}
{groupedEvents.evening.length > 0 && (
<div className={styles.eventGroup}>
<div className={styles.eventGroupHeader}>
<Moon className={styles.eventGroupIcon} />
<h3 className={cn(
styles.eventGroupTitle,
'calendar-day-view-group-title'
)}>
Evening
</h3>
</div>
<div className={styles.eventsList}>
{groupedEvents.evening.map(event => (
<CalendarEventItem
key={event.id}
event={event}
onClick={handleEventClick}
/>
))}
</div>
</div>
)}
)}
</div>
</div>
);
};
@@ -390,27 +372,22 @@ export const CalendarDayView: React.FC<CalendarDayViewProps> = ({
isOpen={isOpen}
onClose={handleClose}
title={formattedDate}
className={cn('calendar-day-view-slide-in', className)}
className={cn(styles.container, className)}
>
<FormPageContent className="calendar-day-view-content">
{renderContent()}
<FormPageContent className="p-0">
<div className="flex flex-col h-full">
{renderContent()}
</div>
</FormPageContent>
<FormPageFooter>
<Button variant="outline" onClick={handleClose}>
Close
</Button>
{onAddEvent && (
<div className="flex justify-end w-full">
<Button onClick={handleAddEvent}>
<PlusCircle className="h-4 w-4 mr-2" />
<PlusCircle className="mr-2 h-4 w-4" />
Add Event
</Button>
)}
</div>
</FormPageFooter>
</FormPage>
{/* Calendar Event Form */}
<CalendarEventForm
isOpen={showEventForm}
onClose={handleEventFormClose}
@@ -420,7 +397,6 @@ export const CalendarDayView: React.FC<CalendarDayViewProps> = ({
babies={babies}
caretakers={caretakers}
contacts={contacts}
familyId={family?.id}
/>
</>
);

View File

@@ -15,7 +15,6 @@ A comprehensive form component for creating and editing calendar events in the B
- Responsive design for mobile and desktop
- Dark mode support
- Accessible UI with proper semantic structure
- Multi-family support with family ID association
## Usage
@@ -90,7 +89,6 @@ Main component for creating and editing calendar events.
| `caretakers` | `Caretaker[]` | Available caretakers to select | Required |
| `contacts` | `Contact[]` | Available contacts to select | Required |
| `isLoading` | `boolean` | Whether the form is in a loading state | `false` |
| `familyId` | `string` | The ID of the family this event belongs to (for multi-family support) | `undefined` |
### CalendarEventFormData
@@ -199,34 +197,6 @@ The component follows a modular structure:
- `calendar-event-form.types.ts` - TypeScript type definitions
- `calendar-event-form.css` - Additional CSS for dark mode and animations
### Multi-Family Support
The component supports multi-family functionality by:
- Accepting a `familyId` prop to associate the calendar event with a specific family
- Including the family ID in the API request payload for create, update, and delete operations
- Adding the family ID to the CalendarEventFormData interface
When using this component in a multi-family context, you should:
1. Import and use the family context to get the current family ID
2. Pass the family ID to the CalendarEventForm component
3. The component will handle sending this ID to the API
```tsx
import { useFamily } from '@/src/context/family';
import { CalendarEventForm } from '@/src/components/forms/CalendarEventForm';
function CalendarPage() {
const { family } = useFamily(); // Get current family from context
return (
<CalendarEventForm
// Other props...
familyId={family?.id} // Pass the current family ID
/>
);
}
```
## Accessibility
The component includes:

View File

@@ -21,7 +21,6 @@ import {
DropdownMenuItem,
} from '@/src/components/ui/dropdown-menu';
import './calendar-event-form.css';
import { useFamily } from '@/src/context/family';
/**
* CalendarEventForm Component
@@ -39,9 +38,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
caretakers,
contacts,
isLoading = false,
familyId,
}) => {
const { family } = useFamily();
// Helper function to get initial form data
const getInitialFormData = (
@@ -168,7 +165,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
const handleCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, checked } = e.target;
setFormData(prev => ({ ...prev, [name]: checked }));
}; // <-- Added missing closing brace
};
// Handle start date/time change with DateTimePicker
const handleStartDateTimeChange = (date: Date) => {
@@ -399,13 +396,8 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
// Handle form submission
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
// Include familyId in the form data when submitting
onSave({
...formData,
familyId: familyId || family?.id || undefined,
});
onSave(formData);
}
};
@@ -414,27 +406,20 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
onClose();
};
if (!isOpen) return null;
return (
<FormPage
isOpen={isOpen}
onClose={onClose}
title={event ? 'Edit Event' : 'Add Event'}
description="Schedule and manage calendar events"
className="calendar-event-form-container"
>
<form onSubmit={handleSubmit} className="flex flex-col h-full">
<FormPageContent className="flex-1">
<div className="space-y-6 pb-20">
<FormPage isOpen={isOpen} onClose={handleClose} title={event ? 'Edit Event' : 'New Event'}>
<form onSubmit={handleSubmit} className="flex flex-col h-full">
<FormPageContent>
<div className="space-y-6">
{/* Event details section */}
<div className={styles.section}>
<h3 className={styles.sectionTitle}>Event Details</h3>
{/* Title */}
<div className={styles.fieldGroup}>
<label
htmlFor="title"
className="form-label"
>
<label htmlFor="title" className={styles.fieldLabel}>
Title
<span className={styles.fieldRequired}>*</span>
</label>
@@ -454,22 +439,16 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
</div>
)}
</div>
{/* Event type */}
<div className={styles.fieldGroup}>
<label
htmlFor="type"
className="form-label"
>
<label htmlFor="type" className={styles.fieldLabel}>
Event Type
<span className={styles.fieldRequired}>*</span>
</label>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
className="w-full justify-between"
>
<Button variant="outline" className="w-full justify-between">
{formData.type.charAt(0) + formData.type.slice(1).toLowerCase().replace('_', ' ')}
</Button>
</DropdownMenuTrigger>
@@ -513,15 +492,12 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
All day event
</label>
</div>
{/* Date and time - each on its own row */}
<div className="space-y-4">
{/* Start Date/Time - Full width */}
<div className={styles.fieldGroup}>
<label
htmlFor="startTime"
className="form-label"
>
<label htmlFor="startTime" className={styles.fieldLabel}>
Start Time
<span className={styles.fieldRequired}>*</span>
</label>
@@ -544,10 +520,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
{/* End Date/Time - Full width */}
<div className={styles.fieldGroup}>
<label
htmlFor="endTime"
className="form-label"
>
<label htmlFor="endTime" className={styles.fieldLabel}>
End Time
</label>
<div className="grid w-full">
@@ -570,10 +543,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
{/* Location */}
<div className={styles.fieldGroup}>
<label
htmlFor="location"
className="form-label"
>
<label htmlFor="location" className={styles.fieldLabel}>
Location
</label>
<div className="relative">
@@ -592,10 +562,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
{/* Description */}
<div className={styles.fieldGroup}>
<label
htmlFor="description"
className="form-label"
>
<label htmlFor="description" className={styles.fieldLabel}>
Description
</label>
<Textarea
@@ -610,10 +577,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
{/* Color */}
<div className={styles.fieldGroup}>
<label
htmlFor="color"
className="form-label"
>
<label htmlFor="color" className={styles.fieldLabel}>
Color
</label>
<div className="flex items-center space-x-2">
@@ -637,9 +601,9 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
</div>
</div>
</div>
{/* Recurrence section - commented out as functionality is not fully implemented yet */}
{/*
{/*
<div className={styles.section}>
<h3 className={styles.sectionTitle}>Recurrence</h3>
@@ -657,7 +621,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
/>
</div>
*/}
{/* Reminder section - commented out as functionality is not fully implemented yet */}
{/*
<div className={styles.section}>
@@ -729,7 +693,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
{/* Babies - Only show if there's more than one active baby */}
{babies.filter(baby => baby.inactive !== true).length > 1 ? (
<div className={styles.fieldGroup}>
<label className="form-label">
<label className={styles.fieldLabel}>
Babies
</label>
<div className="space-y-2">
@@ -789,7 +753,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
{/* Caretakers */}
<div className={styles.fieldGroup}>
<label className="form-label">
<label className={styles.fieldLabel}>
Caretakers
</label>
<div className="space-y-2">
@@ -816,7 +780,7 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
{/* Contacts */}
<div className={styles.fieldGroup}>
<label className="form-label">
<label className={styles.fieldLabel}>
Contacts
</label>
<ContactSelector
@@ -834,73 +798,75 @@ const CalendarEventForm: React.FC<CalendarEventFormProps> = ({
</FormPageContent>
<FormPageFooter>
<div className="flex justify-end space-x-2">
<Button
type="button"
variant="outline"
onClick={handleClose}
disabled={isLoading}
>
Cancel
</Button>
<div className="flex justify-between items-center w-full">
<div>
{event && (
<Button
type="button"
variant="destructive"
onClick={async () => {
if (!event.id) return;
try {
// Delete the event
const response = await fetch(`/api/calendar-event?id=${event.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (data.success) {
// Close the form
onClose();
// Call onSave with a special flag to indicate deletion
// This will trigger a refresh in the parent components
onSave({
...event,
_deleted: true, // Special flag to indicate deletion
});
} else {
console.error('Error deleting event:', data.error);
}
} catch (error) {
console.error('Error deleting event:', error);
}
}}
disabled={isLoading}
>
<Trash2 className="h-4 w-4 mr-1.5" />
Delete
</Button>
)}
</div>
{event && (
<div className="flex justify-end space-x-2">
<Button
type="button"
variant="destructive"
onClick={async () => {
if (!event.id) return;
try {
// Delete the event
const response = await fetch(`/api/calendar-event?id=${event.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ familyId }), // Include familyId in the request body
});
const data = await response.json();
if (data.success) {
// Close the form
onClose();
// Call onSave with a special flag to indicate deletion
// This will trigger a refresh in the parent components
onSave({
...event,
_deleted: true, // Special flag to indicate deletion
familyId, // Include familyId in the event data
});
} else {
console.error('Error deleting event:', data.error);
}
} catch (error) {
console.error('Error deleting event:', error);
}
}}
variant="outline"
onClick={handleClose}
disabled={isLoading}
>
<Trash2 className="h-4 w-4 mr-1.5" />
Delete
Cancel
</Button>
)}
<Button
type="submit"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
Saving...
</>
) : (
'Save Event'
)}
</Button>
<Button
type="submit"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
Saving...
</>
) : (
'Save Event'
)}
</Button>
</div>
</div>
</FormPageFooter>
</form>