Repository: tutur3u/platform
Pull request #2496: Drag to create new calendar event
Author: login: coder1807
Created: 2025-04-24T10:49:59Z | Merged: 2025-04-24T12:08:43Z

Event created by drag and drop but color is not displayed

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Introduced drag-to-create functionality in the calendar, allowing users to create events of custom duration by dragging within a calendar cell.
  - Added real-time visual feedback during event creation, including a preview of the selected time range and duration.

- **Enhancements**
  - Calendar columns now include a data attribute indicating the column's date for improved metadata and potential integrations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx b/apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx
index c374d6acb3..01ad3a6b99 100644
--- a/apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx
+++ b/apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx
@@ -345,7 +345,7 @@ export default function ChallengeCard({
           </div>
 
           <div className="flex flex-col items-center justify-center">
-            <div className="flex items-center text-xs text-muted-foreground">
+            <div className="text-muted-foreground flex items-center text-xs">
               <Clock className="mr-1 h-3 w-3" /> {t('time-remaining')}:
             </div>
             <Countdown
@@ -360,7 +360,7 @@ export default function ChallengeCard({
             className="mb-2"
           />
 
-          <div className="mt-2 text-xs text-muted-foreground">
+          <div className="text-muted-foreground mt-2 text-xs">
             <div className="flex items-center">
               <span>
                 {' '}
@@ -519,12 +519,12 @@ export default function ChallengeCard({
           )}
         </CardHeader>
         <CardContent className="flex-grow">
-          <p className="mb-4 text-muted-foreground">{challenge.description}</p>
+          <p className="text-muted-foreground mb-4">{challenge.description}</p>
 
           <div className="grid gap-2">
             <div className="flex items-center">
-              <Clock className="h-4 w-4 flex-shrink-0 text-primary" />
-              <span className="ml-2 text-sm text-muted-foreground">
+              <Clock className="text-primary h-4 w-4 flex-shrink-0" />
+              <span className="text-muted-foreground ml-2 text-sm">
                 {t('duration')}: {formatDuration(challenge.duration)}
               </span>
             </div>
@@ -535,7 +535,7 @@ export default function ChallengeCard({
               status === 'upcoming' ? (
                 <div className="flex items-center">
                   <AlertCircle className="h-4 w-4 text-indigo-500" />
-                  <span className="ml-2 text-sm text-muted-foreground">
+                  <span className="text-muted-foreground ml-2 text-sm">
                     {t('total-attempts')}: {totalSessions}/
                     {challenge.max_attempts}
                   </span>
@@ -551,7 +551,7 @@ export default function ChallengeCard({
               status === 'upcoming' ? (
                 <div className="flex items-center">
                   <AlertCircle className="h-4 w-4 text-violet-500" />
-                  <span className="ml-2 text-sm text-muted-foreground">
+                  <span className="text-muted-foreground ml-2 text-sm">
                     {t('daily-attempts')}: {dailySessions}/
                     {challenge.max_daily_attempts}
                   </span>
@@ -564,7 +564,7 @@ export default function ChallengeCard({
               {status === 'upcoming' && challenge.previewable_at ? (
                 <div className="mt-2 flex items-center">
                   <Eye className="h-4 w-4 text-amber-500" />
-                  <span className="ml-2 text-sm text-muted-foreground">
+                  <span className="text-muted-foreground ml-2 text-sm">
                     {t('preview-available')}:{' '}
                     {formatDistanceToNow(new Date(challenge.previewable_at), {
                       addSuffix: true,
diff --git a/packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx b/packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx
index 8ee9168b86..0938c9ed6c 100644
--- a/packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx
+++ b/packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx
@@ -2,7 +2,7 @@ import { useCalendar } from '../../../../hooks/use-calendar';
 import { HOUR_HEIGHT } from './config';
 import { cn } from '@tuturuuu/utils/format';
 import { format } from 'date-fns';
-import { useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
 
 interface CalendarCellProps {
   date: string;
@@ -10,8 +10,12 @@ interface CalendarCellProps {
 }
 
 const CalendarCell = ({ date, hour }: CalendarCellProps) => {
-  const { addEmptyEvent } = useCalendar();
+  const { addEmptyEvent, addEmptyEventWithDuration } = useCalendar();
   const [isHovering, setIsHovering] = useState(false);
+  const [isDragging, setIsDragging] = useState(false);
+  const cellRef = useRef<HTMLDivElement>(null);
+  const dragStartRef = useRef<{ date: Date; y: number } | null>(null);
+  const previewRef = useRef<HTMLDivElement | null>(null);
 
   const id = `cell-${date}-${hour}`;
 
@@ -28,9 +32,278 @@ const CalendarCell = ({ date, hour }: CalendarCellProps) => {
     addEmptyEvent(newDate);
   };
 
+  // Round to 15 minute intervals
+  const roundToNearest15Minutes = (date: Date): Date => {
+    const minutes = date.getMinutes();
+    const remainder = minutes % 15;
+    const roundedMinutes =
+      remainder < 8 ? minutes - remainder : minutes + (15 - remainder);
+    const roundedDate = new Date(date);
+    roundedDate.setMinutes(roundedMinutes);
+    roundedDate.setSeconds(0);
+    roundedDate.setMilliseconds(0);
+    return roundedDate;
+  };
+
+  // Convert Y coordinate to time (hour and minutes)
+  const yToTime = useCallback(
+    (y: number): [number, number] => {
+      if (!cellRef.current) return [hour, 0];
+
+      const cellRect = cellRef.current.getBoundingClientRect();
+      const relativeY = y - cellRect.top;
+      const hourFraction = relativeY / HOUR_HEIGHT;
+
+      const hours = Math.floor(hourFraction) + hour;
+      const minutes = Math.round(
+        (hourFraction - Math.floor(hourFraction)) * 60
+      );
+
+      return [hours, minutes];
+    },
+    [hour]
+  );
+
+  // Handle mouse down event - start drag
+  const handleMouseDown = useCallback(
+    (e: MouseEvent) => {
+      // Only handle left click
+      if (e.button !== 0) return;
+
+      e.preventDefault();
+
+      // Initialize drag start information
+      const newDate = new Date(date);
+      const [hours, minutes] = yToTime(e.clientY);
+      newDate.setHours(hours, minutes, 0, 0);
+
+      const roundedDate = roundToNearest15Minutes(newDate);
+      dragStartRef.current = {
+        date: roundedDate,
+        y: e.clientY,
+      };
+
+      setIsDragging(true);
+
+      // Create preview element
+      const preview = document.createElement('div');
+      preview.className =
+        'absolute left-1 right-1 bg-primary/30 border border-primary z-10 rounded-md shadow-md transition-all overflow-hidden';
+      preview.style.top = `${e.clientY - cellRef.current!.getBoundingClientRect().top}px`;
+      preview.style.height = '4px';
+
+      // Add a container for time information
+      const infoContainer = document.createElement('div');
+      infoContainer.className =
+        'w-full h-full flex flex-col justify-between p-1 opacity-0 transition-opacity';
+      preview.appendChild(infoContainer);
+
+      // Add start time label
+      const startTimeLabel = document.createElement('div');
+      startTimeLabel.className =
+        'text-xs font-semibold text-primary-foreground bg-primary/50 px-1 rounded self-start';
+      startTimeLabel.textContent = format(roundedDate, 'h:mm a');
+      infoContainer.appendChild(startTimeLabel);
+
+      // Add duration label
+      const durationLabel = document.createElement('div');
+      durationLabel.className =
+        'text-xs font-semibold text-white px-2 py-1 rounded bg-primary absolute right-1 top-1 whitespace-nowrap';
+      durationLabel.textContent = '0min';
+      preview.appendChild(durationLabel);
+
+      // Add end time label
+      const endTimeLabel = document.createElement('div');
+      endTimeLabel.className =
+        'text-xs font-semibold text-primary-foreground bg-primary/50 px-1 rounded self-end';
+      endTimeLabel.textContent = format(roundedDate, 'h:mm a');
+      infoContainer.appendChild(endTimeLabel);
+
+      cellRef.current!.appendChild(preview);
+      previewRef.current = preview;
+
+      document.body.style.cursor = 'ns-resize';
+      document.body.classList.add('select-none');
+    },
+    [date, yToTime]
+  );
+
+  // Handle mouse move event - update drag preview
+  const handleMouseMove = useCallback(
+    (e: MouseEvent) => {
+      if (!isDragging || !dragStartRef.current || !previewRef.current) return;
+
+      e.preventDefault();
+
+      const cellRect = cellRef.current!.getBoundingClientRect();
+      const startY = dragStartRef.current.y - cellRect.top;
+      const currentY = e.clientY - cellRect.top;
+
+      // Calculate top and height
+      const top = Math.min(startY, currentY);
+      const height = Math.abs(currentY - startY);
+
+      // Update preview element
+      previewRef.current.style.top = `${top}px`;
+      previewRef.current.style.height = `${Math.max(height, 20)}px`;
+
+      // Show info container if height is sufficient
+      const infoContainer = previewRef.current.querySelector('div');
+      if (infoContainer && height > 40) {
+        infoContainer.classList.remove('opacity-0');
+        infoContainer.classList.add('opacity-100');
+      } else if (infoContainer) {
+        infoContainer.classList.remove('opacity-100');
+        infoContainer.classList.add('opacity-0');
+      }
+
+      // Update time labels
+      const startDate = dragStartRef.current.date;
+      const [endHours, endMinutes] = yToTime(e.clientY);
+      const endDate = new Date(date);
+      endDate.setHours(endHours, endMinutes, 0, 0);
+      const roundedEndDate = roundToNearest15Minutes(endDate);
+
+      // Determine which date is actually start and end
+      const isReversed = endDate.getTime() < startDate.getTime();
+      const actualStartDate = isReversed ? roundedEndDate : startDate;
+      const actualEndDate = isReversed ? startDate : roundedEndDate;
+
+      // Calculate duration in minutes
+      const durationMs = actualEndDate.getTime() - actualStartDate.getTime();
+      const durationMinutes = Math.round(durationMs / (1000 * 60));
+
+      // Find the time labels
+      const startTimeLabel =
+        previewRef.current.querySelectorAll('div > div')[0];
+      const durationLabel = previewRef.current.querySelectorAll('div')[1];
+      const endTimeLabel = previewRef.current.querySelectorAll('div > div')[1];
+
+      if (startTimeLabel && endTimeLabel && durationLabel) {
+        startTimeLabel.textContent = format(actualStartDate, 'h:mm a');
+        endTimeLabel.textContent = format(actualEndDate, 'h:mm a');
+
+        // Display different formats based on duration
+        if (durationMinutes < 60) {
+          durationLabel.textContent = `${durationMinutes}min`;
+        } else {
+          const hours = Math.floor(durationMinutes / 60);
+          const minutes = durationMinutes % 60;
+          durationLabel.textContent =
+            minutes > 0 ? `${hours}h ${minutes}min` : `${hours}h`;
+        }
+
+        // Position the duration label based on preview height
+        if (height > 60) {
+          durationLabel.classList.remove('top-1', '-top-6', '-bottom-6');
+          durationLabel.classList.add('top-1');
+        } else if (isReversed) {
+          durationLabel.classList.remove('top-1', '-top-6', '-bottom-6');
+          durationLabel.classList.add('-bottom-6');
+        } else {
+          durationLabel.classList.remove('top-1', '-top-6', '-bottom-6');
+          durationLabel.classList.add('-top-6');
+        }
+      }
+    },
+    [isDragging, date, yToTime]
+  );
+
+  // Handle mouse up event - create event
+  const handleMouseUp = useCallback(
+    (e: MouseEvent) => {
+      if (!isDragging || !dragStartRef.current) return;
+
+      e.preventDefault();
+      setIsDragging(false);
+
+      // Remove preview
+      if (previewRef.current && cellRef.current) {
+        try {
+          cellRef.current.removeChild(previewRef.current);
+        } catch (e) {
+          // The preview may have been moved to another cell
+          // Find it in the document and remove it
+          const preview = document.querySelector(`[id^="event-preview-"]`);
+          if (preview && preview.parentElement) {
+            preview.parentElement.removeChild(preview);
+          }
+        }
+        previewRef.current = null;
+      }
+
+      // Reset cursor
+      document.body.style.cursor = '';
+      document.body.classList.remove('select-none');
+
+      // Calculate event duration
+      const [endHours, endMinutes] = yToTime(e.clientY);
+      const endDate = new Date(date);
+      endDate.setHours(endHours, endMinutes, 0, 0);
+      const roundedEndDate = roundToNearest15Minutes(endDate);
+
+      // Minimum height should be 15 minutes
+      const startDate = dragStartRef.current.date;
+      const duration = Math.abs(roundedEndDate.getTime() - startDate.getTime());
+
+      // Create event if drag is significant (> 5 minutes)
+      if (duration > 5 * 60 * 1000) {
+        const eventStart =
+          startDate.getTime() <= roundedEndDate.getTime()
+            ? startDate
+            : roundedEndDate;
+        const eventEnd =
+          startDate.getTime() <= roundedEndDate.getTime()
+            ? roundedEndDate
+            : startDate;
+
+        // Set minimum duration to 15 minutes
+        if (eventEnd.getTime() - eventStart.getTime() < 15 * 60 * 1000) {
+          const updatedEndDate = new Date(eventStart);
+          updatedEndDate.setMinutes(eventStart.getMinutes() + 15);
+          addEmptyEventWithDuration(eventStart, updatedEndDate);
+        } else {
+          // Create new event with custom duration
+          addEmptyEventWithDuration(eventStart, eventEnd);
+        }
+      }
+
+      // Always reset drag state
+      dragStartRef.current = null;
+    },
+    [isDragging, date, yToTime, addEmptyEventWithDuration]
+  );
+
+  // Set up event listeners
+  useEffect(() => {
+    const cell = cellRef.current;
+    if (!cell) return;
+
+    cell.addEventListener('mousedown', handleMouseDown);
+    document.addEventListener('mousemove', handleMouseMove);
+    document.addEventListener('mouseup', handleMouseUp);
+
+    return () => {
+      cell.removeEventListener('mousedown', handleMouseDown);
+      document.removeEventListener('mousemove', handleMouseMove);
+      document.removeEventListener('mouseup', handleMouseUp);
+
+      // Clean up any remaining preview
+      if (previewRef.current && cellRef.current) {
+        try {
+          cellRef.current.removeChild(previewRef.current);
+        } catch (e) {
+          // Ignore if already removed
+        }
+        previewRef.current = null;
+      }
+    };
+  }, [handleMouseDown, handleMouseMove, handleMouseUp]);
+
   return (
     <div
       id={id}
+      ref={cellRef}
       className={cn(
         'calendar-cell relative transition-colors',
         hour !== 0 && 'border-border/30 border-t',
diff --git a/packages/ui/src/components/ui/legacy/calendar/CalendarColumn.tsx b/packages/ui/src/components/ui/legacy/calendar/CalendarColumn.tsx
index 7cdf010859..792721c5e3 100644
--- a/packages/ui/src/components/ui/legacy/calendar/CalendarColumn.tsx
+++ b/packages/ui/src/components/ui/legacy/calendar/CalendarColumn.tsx
@@ -21,6 +21,7 @@ const CalendarColumn = ({ date, last }: CalendarColumnProps) => {
         minWidth: '120px',
         height: `${DAY_HEIGHT}px`, // 24 hours * 80px = 1920px
       }}
+      data-column-date={date}
     >
       {hours.map((hour) => (
         <CalendarCell key={`${date}-${hour}`} date={date} hour={hour} />
diff --git a/packages/ui/src/hooks/use-calendar.tsx b/packages/ui/src/hooks/use-calendar.tsx
index 65d6bd35bb..4def3e1f41 100644
--- a/packages/ui/src/hooks/use-calendar.tsx
+++ b/packages/ui/src/hooks/use-calendar.tsx
@@ -49,6 +49,7 @@ const CalendarContext = createContext<{
     event: Omit<CalendarEvent, 'id'>
   ) => Promise<CalendarEvent | undefined>;
   addEmptyEvent: (date: Date) => CalendarEvent;
+  addEmptyEventWithDuration: (startDate: Date, endDate: Date) => CalendarEvent;
   updateEvent: (
     eventId: string,
     data: Partial<CalendarEvent>
@@ -77,6 +78,7 @@ const CalendarContext = createContext<{
   getEventLevel: () => 0,
   addEvent: () => Promise.resolve({} as CalendarEvent),
   addEmptyEvent: () => ({}) as CalendarEvent,
+  addEmptyEventWithDuration: () => ({}) as CalendarEvent,
   updateEvent: () => Promise.resolve({} as CalendarEvent),
   deleteEvent: () => Promise.resolve(),
   isModalOpen: false,
@@ -596,6 +598,40 @@ export const CalendarProvider = ({
     [ws?.id, settings.taskSettings, settings.categoryColors]
   );
 
+  const addEmptyEventWithDuration = useCallback(
+    (startDate: Date, endDate: Date) => {
+      // Round start and end times to nearest 15-minute interval
+      const roundedStartDate = roundToNearest15Minutes(startDate);
+      const roundedEndDate = roundToNearest15Minutes(endDate);
+
+      // Use default color from settings
+      const defaultColor =
+        settings.categoryColors.categories[0]?.color || 'BLUE';
+
+      // Create a new event with default values
+      const newEvent: CalendarEvent = {
+        id: 'new',
+        title: '',
+        description: '',
+        start_at: roundedStartDate.toISOString(),
+        end_at: roundedEndDate.toISOString(),
+        color: defaultColor,
+        ws_id: ws?.id || '',
+      };
+
+      // Store the pending new event
+      setPendingNewEvent(newEvent);
+      setActiveEventId('new');
+
+      // Open the modal with the pending event
+      setModalHidden(false);
+
+      // Return the pending event object
+      return newEvent as CalendarEvent;
+    },
+    [ws?.id, settings.categoryColors]
+  );
+
   // Process the update queue
   const processUpdateQueue = useCallback(async () => {
     if (isProcessingQueueRef.current || updateQueueRef.current.length === 0) {
@@ -1179,6 +1215,7 @@ export const CalendarProvider = ({
 
     addEvent,
     addEmptyEvent,
+    addEmptyEventWithDuration,
     updateEvent,
     deleteEvent,
 
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a6c065c280..c0a23bb01e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -25,7 +25,7 @@ importers:
         version: link:packages/typescript-config
       '@vitest/coverage-v8':
         specifier: ^3.1.2
-        version: 3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))
+        version: 3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.1.0))(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))
       eslint:
         specifier: ^9.25.1
         version: 9.25.1(jiti@2.4.2)
@@ -417,10 +417,10 @@ importers:
         version: 2.7.23
       '@vercel/analytics':
         specifier: ^1.5.0
-        version: 1.5.0(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
+        version: 1.5.0(next@15.3.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
       '@vercel/speed-insights':
         specifier: ^1.2.0
-        version: 1.2.0(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
+        version: 1.2.0(next@15.3.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
       babel-plugin-react-compiler:
         specifier: 19.0.0-beta-21e868a-20250216
         version: 19.0.0-beta-21e868a-20250216
@@ -468,10 +468,10 @@ importers:
         version: 1.0.0
       next:
         specifier: ^15.3.1
-        version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+        version: 15.3.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
       next-intl:
         specifier: ^3.26.5
-        version: 3.26.5(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
+        version: 3.26.5(next@15.3.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
       next-themes:
         specifier: ^0.4.6
         version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -489,7 +489,7 @@ importers:
         version: 19.1.0(react@19.1.0)
       react-email:
         specifier: ^4.0.7
-        version: 4.0.7(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+        version: 4.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
       react-intersection-observer:
         specifier: ^9.16.0
         version: 9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -1109,10 +1109,10 @@ importers:
         version: 2.7.23
       '@vercel/analytics':
         specifier: ^1.5.0
-        version: 1.5.0(next@15.3.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
+        version: 1.5.0(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
       '@vercel/speed-insights':
         specifier: ^1.2.0
-        version: 1.2.0(next@15.3.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
+        version: 1.2.0(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
       babel-plugin-react-compiler:
         specifier: 19.0.0-beta-21e868a-20250216
         version: 19.0.0-beta-21e868a-20250216
@@ -1136,10 +1136,10 @@ importers:
         version: 1.0.0
       next:
         specifier: ^15.3.1
-        version: 15.3.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+        version: 15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
       next-intl:
         specifier: ^3.26.5
-        version: 3.26.5(next@15.3.1(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
+        version: 3.26.5(next@15.3.1(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react@19.1.0)
       next-themes:
         specifier: ^0.4.6
         version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -1157,7 +1157,7 @@ importers:
         version: 19.1.0(react@19.1.0)
       react-email:
         specifier: ^4.0.7
-        version: 4.0.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
+        version: 4.0.7(@babel/core@7.26.10)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
       react-intersection-observer:
         specifier: ^9.16.0
         version: 9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -14492,7 +14492,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@vitest/coverage-v8@3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))':
+  '@vitest/coverage-v8@3.1.2(vitest@3.1.2(@types/debug@4.1.12)(@types/node@22.14.1)(jiti@2.4.2)(jsdom@26.1.0(canvas@3.1.0))(lightningcss@1.29.2)(terser@5.39.0)(yaml@2.7.1))':
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@bcoe/v8-coverage': 1.0.2
@@ -15769,7 +15769,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.12.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)):
+  eslint-module-utils@2.12.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2)):
     dependencies:
       debug: 3.2.7
     optionalDependencies:
@@ -15791,7 +15791,7 @@ snapshots:
       doctrine: 2.1.0
       eslint: 9.25.1(jiti@2.4.2)
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
+      eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.25.1(jiti@2.4.2))
       hasown: 2.0.2
       is-core-module: 2.16.1
       is-glob: 4.0.3


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>packages/ui/src/hooks/use-calendar.tsx (1)</summary><blockquote>

`601-633`: **Duplicate logic between the two “add empty event” helpers – consider DRYing up**

`addEmptyEventWithDuration` is almost a copy-paste of `addEmptyEvent` with only:

* start/end arguments instead of a single date
* no default-duration logic.

Keeping two versions increases the chance they diverge unintentionally (e.g. when default color / rounding rule changes).

Diff-sketch to refactor:

```diff
-const buildEmptyEvent = (
-  start: Date,
-  end: Date,
-  wsId: string,
-  defaultColor: SupportedColor,
-) : CalendarEvent => ({
-  id: 'new',
-  title: '',
-  description: '',
-  start_at: start.toISOString(),
-  end_at: end.toISOString(),
-  color: defaultColor,
-  ws_id: wsId,
-});
-
-const addEmptyEvent = ...
-const addEmptyEventWithDuration = ...
+const buildEmptyEvent = (...args) => { /* impl above */ }
+
+const openPendingEvent = (evt: CalendarEvent) => {
+  setPendingNewEvent(evt);
+  setActiveEventId('new');
+  setModalHidden(false);
+  return evt;
+};
+
+const addEmptyEvent = useCallback((date: Date) => {
+  /* pick start/end based on default duration, then: */
+  return openPendingEvent(
+    buildEmptyEvent(start_at, end_at, ws?.id || '', defaultColor),
+  );
+}, [...]);
+
+const addEmptyEventWithDuration = useCallback((start, end) => {
+  return openPendingEvent(
+    buildEmptyEvent(
+      roundToNearest15Minutes(start),
+      roundToNearest15Minutes(end),
+      ws?.id || '',
+      defaultColor,
+    ),
+  );
+}, [...]);
```

This tiny utility eliminates the duplicated object-assembly and modal-opening code.

</blockquote></details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx (3)</summary><blockquote>

`35-46`: **Local `roundToNearest15Minutes` duplicates global helper**

The same rounding helper lives in `use-calendar.tsx`.  Importing it (or moving to a shared `utils/date.ts`) avoids copy-paste divergence and shrinks bundle size.

---

`132-209`: **Heavy DOM queries inside `mousemove` – cache references**

`querySelector*` is executed on every mouse-move which can fire hundreds of times per second, causing unnecessary layout thrashing.  
Store the three label elements in refs right after creation instead of re-querying.

---

`151-158`: **Optional chaining would silence null-checks and matches linter hint**

Biome suggested switching to optional chaining.  Example:

```diff
-  if (infoContainer && height > 40) {
+  if (infoContainer?.classList) {
+    /* then compare height etc. */
   }
```

This removes the false-positive null-check with no behavioural change.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 80b94711291136dfdfdfc8d7841e0fb9231c047b and 7fd50e13654be3dfe5e3d2bd2ccdf060f656481b.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `pnpm-lock.yaml` is excluded by `!**/pnpm-lock.yaml`

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx` (2 hunks)
* `packages/ui/src/components/ui/legacy/calendar/CalendarColumn.tsx` (1 hunks)
* `packages/ui/src/hooks/use-calendar.tsx` (4 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (2)</summary>

<details>
<summary>packages/ui/src/hooks/use-calendar.tsx (2)</summary><blockquote>

<details>
<summary>packages/types/src/primitives/calendar-event.ts (1)</summary>

* `CalendarEvent` (5-30)

</details>
<details>
<summary>packages/ai/src/calendar/events.ts (1)</summary>

* `CalendarEvent` (32-32)

</details>

</blockquote></details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx (2)</summary><blockquote>

<details>
<summary>packages/ui/src/hooks/use-calendar.tsx (1)</summary>

* `useCalendar` (1264-1269)

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/config.ts (1)</summary>

* `HOUR_HEIGHT` (1-1)

</details>

</blockquote></details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx</summary>

[error] 228-228: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (2)</summary>

* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Deploy-Preview

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarColumn.tsx (1)</summary>

`24-25`: **Nice addition of `data-column-date` attribute**

Exposing the date as a data attribute will simplify DOM-level calculations (e.g. hit-testing while dragging). No issues spotted.

</details>
<details>
<summary>packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx (1)</summary>

`214-275`: **Edge-cases around minimum duration & cross-midnight**

1. If a user drags 5–14 min you force 15 min by adding to the endDate, but you don’t clamp to day-end, so an event from 23:55 could become 00:10 of the next day while still displayed in the previous column.  
2. Conversely, a reversed drag that starts at 00:03 and ends at 23:58 will flip start/end but might exceed 24 h.

Consider:

* Clamping `eventEnd` to `startDate.setMinutes(start+14)` **or** day-boundary whichever is smaller.
* Rejecting spans that exceed 24 h (or silently shrink them).


Would you like a follow-up diff that enforces these constraints?

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 228-228: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T10:53:38Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx (1)</summary><blockquote>

`348-348`: **UI Consistency and Test Coverage Recommendation**  
Reordering the `text-muted-foreground` class to the front here improves consistency with other components. Consider adding a snapshot or visual regression test for `ChallengeCard` to cover this styling change and prevent future regressions.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 348-348: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L348
Added line #L348 was not covered by tests

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7fd50e13654be3dfe5e3d2bd2ccdf060f656481b and d5be6469a2b3761c8ec30ca214c65dbe451d6770.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx` (6 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx</summary>

[warning] 348-348: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L348
Added line #L348 was not covered by tests

---

[warning] 363-363: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L363
Added line #L363 was not covered by tests

---

[warning] 522-522: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L522
Added line #L522 was not covered by tests

---

[warning] 526-527: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L526-L527
Added lines #L526 - L527 were not covered by tests

---

[warning] 538-538: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L538
Added line #L538 was not covered by tests

---

[warning] 554-554: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L554
Added line #L554 was not covered by tests

---

[warning] 567-567: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L567
Added line #L567 was not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (7)</summary>

* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview
* GitHub Check: Verify generated types
* GitHub Check: Deploy-Preview
* GitHub Check: Deploy-Preview

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx (6)</summary>

`363-363`: **UI Consistency: CSS Class Ordering**  
Moving `text-muted-foreground` before other utility classes ensures a uniform styling convention across the component.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 363-363: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L363
Added line #L363 was not covered by tests

</details>

</details>

---

`522-522`: **UI Consistency: Description Styling**  
Placing `text-muted-foreground` first in the paragraph’s class list aligns with the component’s established styling pattern.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 522-522: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L522
Added line #L522 was not covered by tests

</details>

</details>

---

`526-527`: **UI Consistency: Icon and Text Styling**  
Reordering the classes on the `<Clock>` icon and its sibling `<span>` enhances readability and maintains a consistent utility-first class order.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 526-527: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L526-L527
Added lines #L526 - L527 were not covered by tests

</details>

</details>

---

`538-538`: **UI Consistency: Total Attempts Icon**  
Adjusting the class order on the `<AlertCircle>` icon keeps the styling approach consistent with the rest of the markup.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 538-538: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L538
Added line #L538 was not covered by tests

</details>

</details>

---

`554-554`: **UI Consistency: Daily Attempts Icon**  
Reordering the icon and text classes preserves the predictable class ordering convention throughout the component.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 554-554: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L554
Added line #L554 was not covered by tests

</details>

</details>

---

`567-567`: **UI Consistency: Preview Availability Icon**  
Moving the `text-amber-500` class respects the component’s standardized utility-class ordering, aiding future maintenance.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 567-567: apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx#L567
Added line #L567 was not covered by tests

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-24T12:06:24Z
user: login: vhpx
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-24T12:08:15Z

Review comments:
diff_hunk: @@ -49,6 +49,7 @@ const CalendarContext = createContext<{
     event: Omit<CalendarEvent, 'id'>
   ) => Promise<CalendarEvent | undefined>;
   addEmptyEvent: (date: Date) => CalendarEvent;
+  addEmptyEventWithDuration: (startDate: Date, endDate: Date) => CalendarEvent;
   updateEvent: (
path: packages/ui/src/hooks/use-calendar.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Context API extension looks good, but don’t forget consumer-side type narrowing**

`addEmptyEventWithDuration` has been correctly added to the context interface and default value.  
Please make sure every current consumer of `useCalendar()` is re-typed so TypeScript can surface missing implementations instead of silently using `any`.



Run the quick grep below – if it returns anything, some call-sites haven’t been recompiled yet:




Also applies to: 81-82

---

🏁 Script executed:

```shell
#!/usr/bin/env bash
# Find usages that still reference the old overload list
rg --fixed-strings "addEmptyEventWithDuration(" | wc -l
```

Length of output: 59

---

I’ll locate the exact call-sites still using `addEmptyEventWithDuration` so we can update them:

---

🏁 Script executed:

```shell
#!/usr/bin/env bash
# Show file paths and line numbers for remaining usages
rg --fixed-strings -n 'addEmptyEventWithDuration('
```

Length of output: 310

---



</details>

**Remaining untyped `addEmptyEventWithDuration` usages**

Our grep shows two call-sites still using the old overload list—these need to be updated so TypeScript can enforce the new signature:

• packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx:264  
• packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx:267  

Please re-type these hooks calls against the updated `useCalendar()` interface and verify that your TS build surfaces any missing implementations.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T10:53:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,9 +32,278 @@ const CalendarCell = ({ date, hour }: CalendarCellProps) => {
     addEmptyEvent(newDate);
   };
 
+  // Round to 15 minute intervals
+  const roundToNearest15Minutes = (date: Date): Date => {
+    const minutes = date.getMinutes();
+    const remainder = minutes % 15;
+    const roundedMinutes =
+      remainder < 8 ? minutes - remainder : minutes + (15 - remainder);
+    const roundedDate = new Date(date);
+    roundedDate.setMinutes(roundedMinutes);
+    roundedDate.setSeconds(0);
+    roundedDate.setMilliseconds(0);
+    return roundedDate;
+  };
+
+  // Convert Y coordinate to time (hour and minutes)
+  const yToTime = useCallback(
+    (y: number): [number, number] => {
+      if (!cellRef.current) return [hour, 0];
+
+      const cellRect = cellRef.current.getBoundingClientRect();
+      const relativeY = y - cellRect.top;
+      const hourFraction = relativeY / HOUR_HEIGHT;
+
+      const hours = Math.floor(hourFraction) + hour;
+      const minutes = Math.round(
+        (hourFraction - Math.floor(hourFraction)) * 60
+      );
+
+      return [hours, minutes];
+    },
+    [hour]
+  );
+
+  // Handle mouse down event - start drag
+  const handleMouseDown = useCallback(
+    (e: MouseEvent) => {
+      // Only handle left click
+      if (e.button !== 0) return;
+
+      e.preventDefault();
+
+      // Initialize drag start information
+      const newDate = new Date(date);
+      const [hours, minutes] = yToTime(e.clientY);
+      newDate.setHours(hours, minutes, 0, 0);
+
+      const roundedDate = roundToNearest15Minutes(newDate);
+      dragStartRef.current = {
+        date: roundedDate,
+        y: e.clientY,
+      };
+
+      setIsDragging(true);
+
+      // Create preview element
+      const preview = document.createElement('div');
+      preview.className =
+        'absolute left-1 right-1 bg-primary/30 border border-primary z-10 rounded-md shadow-md transition-all overflow-hidden';
+      preview.style.top = `${e.clientY - cellRef.current!.getBoundingClientRect().top}px`;
+      preview.style.height = '4px';
+
+      // Add a container for time information
path: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Preview element is created without an id but removed later via an id-based selector**

`handleMouseUp` falls back to  
```ts
document.querySelector(`[id^="event-preview-"]`)
```  
yet `handleMouseDown` never assigns such an `id`, so the fallback never matches.

Either assign the id when creating the element or drop the fallback removal altogether.


```diff
-const preview = document.createElement('div');
+const preview = document.createElement('div');
+preview.id = `event-preview-${Date.now()}`;
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T10:53:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,9 +32,278 @@
     addEmptyEvent(newDate);
   };
 
+  // Round to 15 minute intervals
+  const roundToNearest15Minutes = (date: Date): Date => {
+    const minutes = date.getMinutes();
+    const remainder = minutes % 15;
+    const roundedMinutes =
+      remainder < 8 ? minutes - remainder : minutes + (15 - remainder);
+    const roundedDate = new Date(date);
+    roundedDate.setMinutes(roundedMinutes);
+    roundedDate.setSeconds(0);
+    roundedDate.setMilliseconds(0);
+    return roundedDate;
+  };
+
+  // Convert Y coordinate to time (hour and minutes)
+  const yToTime = useCallback(
+    (y: number): [number, number] => {
+      if (!cellRef.current) return [hour, 0];
+
+      const cellRect = cellRef.current.getBoundingClientRect();
+      const relativeY = y - cellRect.top;
+      const hourFraction = relativeY / HOUR_HEIGHT;
+
+      const hours = Math.floor(hourFraction) + hour;
+      const minutes = Math.round(
+        (hourFraction - Math.floor(hourFraction)) * 60
+      );
+
+      return [hours, minutes];
+    },
+    [hour]
+  );
+
path: packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`yToTime` needs clamping to avoid negative hours / > 23**

When the pointer is above the start cell or several rows below, `relativeY` becomes negative or > `HOUR_HEIGHT`, generating hour values outside `[0, 23]`.  
Add `Math.max(0, Math.min(23, computedHour))` and normalize minutes to `<60` to prevent impossible datetimes.


```diff
-const hours = Math.floor(hourFraction) + hour;
+const hours = Math.max(
+  0,
+  Math.min(23, Math.floor(hourFraction) + hour),
+);
 
-const minutes = Math.round(/* … */);
+const minutes = Math.min(59, Math.round(/* … */));
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const yToTime = useCallback(
    (y: number): [number, number] => {
      if (!cellRef.current) return [hour, 0];

      const cellRect = cellRef.current.getBoundingClientRect();
      const relativeY = y - cellRect.top;
      const hourFraction = relativeY / HOUR_HEIGHT;

      const hours = Math.max(
        0,
        Math.min(23, Math.floor(hourFraction) + hour),
      );

      const minutes = Math.min(
        59,
        Math.round((hourFraction - Math.floor(hourFraction)) * 60)
      );

      return [hours, minutes];
    },
    [hour]
  );
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-24T10:53:37Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-24T10:50:06Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces drag-based event creation to the calendar UI. The `CalendarCell` component now allows users to click and drag to select a time range, visually preview the selection, and create an event of the chosen duration. Supporting logic includes new state management, mouse event handlers, and utility functions for time rounding. The `useCalendar` hook is extended with an `addEmptyEventWithDuration` method to create events spanning a selected time range. Additionally, `CalendarColumn` now includes a data attribute for its date, aiding DOM metadata. Minor styling reorderings were applied in the `ChallengeCard` component without affecting functionality.

## Changes

| File(s)                                                                                     | Change Summary                                                                                                      |
|--------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------|
| packages/ui/src/components/ui/legacy/calendar/CalendarCell.tsx                             | Enhanced to support drag-to-create events: added state, refs, mouse event handlers, time rounding, and preview UI.  |
| packages/ui/src/hooks/use-calendar.tsx                                                     | Added `addEmptyEventWithDuration` to context, allowing event creation for a selected time range.                    |
| packages/ui/src/components/ui/legacy/calendar/CalendarColumn.tsx                           | Added `data-column-date` attribute to root div for DOM metadata.                                                    |
| apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx                        | Reordered CSS class names for text color and layout styling; no functional changes.                                 |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant CalendarCell
    participant useCalendar

    User->>CalendarCell: Mouse down (start drag)
    User->>CalendarCell: Mouse move (drag to select time range)
    CalendarCell-->>CalendarCell: Update preview UI and times
    User->>CalendarCell: Mouse up (end drag)
    CalendarCell->>useCalendar: addEmptyEventWithDuration(start, end)
    useCalendar-->>CalendarCell: Create and open event modal
```

## Poem

> In the garden of time, where calendars dwell,  
> A rabbit now drags, and events swiftly swell.  
> With fifteen-minute hops, durations align,  
> Previewing the meeting—oh, how divine!  
> Columns now whisper their dates in the breeze,  
> While new hooks make scheduling a breeze.  
> 🐇✨

<!-- walkthrough_end -->
<!-- This is an auto-generated comment: all tool run failures by coderabbit.ai -->

> [!WARNING]
> There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.
> 
> <details>
> <summary>🔧 ESLint</summary>
> 
> > If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.
> 
> <details>
> <summary>apps/nova/src/app/[locale]/(dashboard)/challenges/challengeCard.tsx</summary>
> 
> 
> Oops! Something went wrong! :(
> 
> ESLint: 9.25.1
> 
> Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@typescript-eslint/parser' imported from /eslint.config.mjs
>     at packageResolve (node:internal/modules/esm/resolve:839:9)
>     at moduleResolve (node:internal/modules/esm/resolve:908:18)
>     at defaultResolve (node:internal/modules/esm/resolve:1038:11)
>     at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:557:12)
>     at ModuleLoader.resolve (node:internal/modules/esm/loader:525:25)
>     at ModuleLoader.getModuleJob (node:internal/modules/esm/loader:246:38)
>     at ModuleJob._link (node:internal/modules/esm/module_job:126:49)
> 
> </details>
> 
> </details>

<!-- end of auto-generated comment: all tool run failures by coderabbit.ai -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=tutur3u/platform&utm_content=2496):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: graphite-app[bot]
created_at: 2025-04-24T10:50:07Z
body: ## How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:
- <kbd>merge-queue</kbd> - adds this PR to the back of the merge queue
- <kbd>hotfix</kbd> - for urgent hot fixes, skip the queue and merge this PR next

You must have a Graphite account in order to use the merge queue. Sign up using [this link](https://app.graphite.dev/invite/github/tutur3u?ref=merge-queue-instructions-comment&prId=6137686680).

<h2></h2>
<sub>

An organization admin has enabled the [Graphite Merge Queue](https://graphite.dev/docs/graphite-merge-queue) in this repository.

</sub>

<sub>

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

</sub>
author_association: NONE
user: login: codecov[bot]
created_at: 2025-04-24T10:54:00Z
body: ## [Codecov](https://app.codecov.io/gh/tutur3u/platform/pull/2496?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) Report
Attention: Patch coverage is `0%` with `231 lines` in your changes missing coverage. Please review.
> Project coverage is 1.05%. Comparing base [(`2c3cfd0`)](https://app.codecov.io/gh/tutur3u/platform/commit/2c3cfd01c353fc312dce09ba2cc9f32de0bb0ded?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) to head [(`d5be646`)](https://app.codecov.io/gh/tutur3u/platform/commit/d5be6469a2b3761c8ec30ca214c65dbe451d6770?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u).
> Report is 28 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/tutur3u/platform/pull/2496?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) | Patch % | Lines |
|---|---|---|
| [...src/components/ui/legacy/calendar/CalendarCell.tsx](https://app.codecov.io/gh/tutur3u/platform/pull/2496?src=pr&el=tree&filepath=packages%2Fui%2Fsrc%2Fcomponents%2Fui%2Flegacy%2Fcalendar%2FCalendarCell.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u#diff-cGFja2FnZXMvdWkvc3JjL2NvbXBvbmVudHMvdWkvbGVnYWN5L2NhbGVuZGFyL0NhbGVuZGFyQ2VsbC50c3g=) | 0.00% | [198 Missing :warning: ](https://app.codecov.io/gh/tutur3u/platform/pull/2496?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) |
| [packages/ui/src/hooks/use-calendar.tsx](https://app.codecov.io/gh/tutur3u/platform/pull/2496?src=pr&el=tree&filepath=packages%2Fui%2Fsrc%2Fhooks%2Fuse-calendar.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u#diff-cGFja2FnZXMvdWkvc3JjL2hvb2tzL3VzZS1jYWxlbmRhci50c3g=) | 0.00% | [24 Missing :warning: ](https://app.codecov.io/gh/tutur3u/platform/pull/2496?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) |
| [.../[locale]/(dashboard)/challenges/challengeCard.tsx](https://app.codecov.io/gh/tutur3u/platform/pull/2496?src=pr&el=tree&filepath=apps%2Fnova%2Fsrc%2Fapp%2F%5Blocale%5D%2F%28dashboard%29%2Fchallenges%2FchallengeCard.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u#diff-YXBwcy9ub3ZhL3NyYy9hcHAvW2xvY2FsZV0vKGRhc2hib2FyZCkvY2hhbGxlbmdlcy9jaGFsbGVuZ2VDYXJkLnRzeA==) | 0.00% | [8 Missing :warning: ](https://app.codecov.io/gh/tutur3u/platform/pull/2496?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) |
| [...c/components/ui/legacy/calendar/CalendarColumn.tsx](https://app.codecov.io/gh/tutur3u/platform/pull/2496?src=pr&el=tree&filepath=packages%2Fui%2Fsrc%2Fcomponents%2Fui%2Flegacy%2Fcalendar%2FCalendarColumn.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u#diff-cGFja2FnZXMvdWkvc3JjL2NvbXBvbmVudHMvdWkvbGVnYWN5L2NhbGVuZGFyL0NhbGVuZGFyQ29sdW1uLnRzeA==) | 0.00% | [1 Missing :warning: ](https://app.codecov.io/gh/tutur3u/platform/pull/2496?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #2496      +/-   ##
==========================================
- Coverage    1.05%    1.05%   -0.01%     
==========================================
  Files        1893     1893              
  Lines      192210   192431     +221     
  Branches     2309     2309              
==========================================
  Hits         2037     2037              
- Misses     188618   188839     +221     
  Partials     1555     1555              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/tutur3u/platform/pull/2496?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=tutur3u).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE