Display events for a single day in a time-based grid (24 hours)
Create, edit, and delete events via UI
Drag and drop events to reschedule
Resize events to change duration
Handle overlapping events (visual stacking)
Support recurring events
Show multiple calendars with color coding
Real-time collaboration (shared calendars)
All-day events section
Time zone support
Non-Functional Requirements
Performance: Render 50+ events without lag
Responsiveness: Smooth drag/resize at 60fps
Real-time: Updates within 1 second for shared calendars
Offline: View and create events offline, sync when online
Accessibility: Full keyboard navigation, screen reader support
Capacity Estimation
Daily Active Users: 500 million
Events per user: ~10-20 visible in day view
Peak concurrent users: 50 million
API calls per day view: 3-5 (events, calendars, settings)
Event updates per minute (peak): 10 million
┌─────────────────────────────────────────────────────────────────────────────┐
│ TIME ZONE HANDLING │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ RULE: Store in UTC, Display in User's Timezone │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ Database │ │
│ │ ──────── │ │
│ │ start_time: 2024-12-22T17:00:00Z (UTC) │ │
│ │ │ │
│ │ User in NYC (EST, UTC-5) │ │
│ │ ───────────────────────── │ │
│ │ Display: Dec 22, 12:00 PM │ │
│ │ │ │
│ │ User in London (GMT, UTC+0) │ │
│ │ ────────────────────────── │ │
│ │ Display: Dec 22, 5:00 PM │ │
│ │ │ │
│ │ User in Tokyo (JST, UTC+9) │ │
│ │ ────────────────────────── │ │
│ │ Display: Dec 23, 2:00 AM │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Edge Cases: │
│ ─────────── │
│ • DST transitions (event at 2:30 AM when clocks jump) │
│ • User travels (viewing calendar in different TZ) │
│ • Recurring event across DST boundary │
│ • All-day events (should stay on same date in all TZ) │
│ │
│ Solutions: │
│ ────────── │
│ • Use date-fns-tz or Luxon for TZ-aware operations │
│ • Store user's preferred TZ in profile │
│ • All-day events: store as date only, no time component │
│ • Show "event created in [TZ]" for cross-TZ calendars │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Edge Cases
┌─────────────────────────────────────────────────────────────────────────────┐
│ EDGE CASES │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Overlapping Events (Conflict Visualization) │
│ • Calculate overlap groups │
│ • Split width equally among overlapping events │
│ • Offset left position to show all events │
│ │
│ 2. Multi-day Events in Day View │
│ • Show as all-day event spanning full day │
│ • Indicate "continues from yesterday" / "continues tomorrow" │
│ │
│ 3. Very Long Events (24+ hours) │
│ • Treat as multi-day, show in all-day section │
│ │
│ 4. 0-Duration Events │
│ • Render as minimum height (15 min visual) │
│ • Show special icon indicating instant event │
│ │
│ 5. Midnight-Crossing Events │
│ • Split display across two days │
│ • Store as single event, render as two visuals │
│ │
│ 6. Concurrent Edits (Conflict Resolution) │
│ • Use version numbers (optimistic locking) │
│ • Show "Event was modified" dialog │
│ • Options: Keep mine, Keep theirs, View diff │
│ │
│ 7. Recurring Event Exceptions │
│ • "Edit this instance" vs "Edit all instances" │
│ • "Delete this instance" vs "Delete following" vs "Delete all" │
│ │
│ 8. Offline Mode │
│ • Queue mutations in IndexedDB │
│ • Show pending changes with special indicator │
│ • Sync and handle conflicts when back online │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
11. Interview Cross-Questions
Common Questions & Answers
┌─────────────────────────────────────────────────────────────────────────────┐
│ INTERVIEW CROSS-QUESTIONS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Q: Why WebSocket instead of Long Polling for calendar sync? │
│ A: • Calendar is inherently collaborative │
│ • Bi-directional needed (client sends updates too) │
│ • Lower latency for real-time feel │
│ • More efficient for sustained connections │
│ • Long polling would create reconnection overhead │
│ │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Q: How would you handle recurring events at scale? │
│ A: • Store RRULE, expand on read │
│ • Never materialize infinite series │
│ • Pre-expand commonly accessed ranges (next 90 days) │
│ • Store exceptions separately, merge at query time │
│ • Use rrule.js library for expansion │
│ │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Q: How do you make drag & drop smooth at 60fps? │
│ A: • Use CSS transforms instead of top/left │
│ • throttle with requestAnimationFrame │
│ • will-change for GPU layer promotion │
│ • Ghost element pattern to avoid DOM thrashing │
│ • Delay React state update until drop │
│ │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Q: How do you handle overlapping events? │
│ A: • Sort events by start time: O(n log n) │
│ • Sweep line algorithm to find overlaps │
│ • Assign columns using greedy coloring │
│ • Calculate width = 100% / numOverlapping │
│ • Calculate left offset = column * width │
│ │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Q: Why PostgreSQL instead of MongoDB for events? │
│ A: • Range queries on timestamps are crucial │
│ • Need ACID for preventing double-booking │
│ • GiST index for efficient overlap detection │
│ • Complex joins (events + calendars + permissions) │
│ • MongoDB lacks efficient range scans on dates │
│ │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Q: How would you implement offline support? │
│ A: • Service Worker with Cache API for static assets │
│ • IndexedDB for event data and pending mutations │
│ • Queue creates/updates/deletes while offline │
│ • On reconnect: sync queue with server │
│ • Handle conflicts: last-write-wins or prompt user │
│ │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Q: How do you handle timezone edge cases? │
│ A: • Always store in UTC │
│ • Convert to user's TZ only for display │
│ • Use date-fns-tz or Luxon for conversions │
│ • All-day events: store as date only, not datetime │
│ • Recurring: expand in original TZ, then convert │
│ │
│ ───────────────────────────────────────────────────────────────────────── │
│ │
│ Q: What metrics would you track? │
│ A: • Render time for day view (< 100ms target) │
│ • Drag/resize frame rate (target 60fps) │
│ • Time to interactive │
│ • API latency (p50, p95, p99) │
│ • WebSocket reconnection rate │
│ • Conflict resolution frequency │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ KEYBOARD NAVIGATION MAP │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Global Shortcuts: │
│ ───────────────── │
│ T → Go to today │
│ J / ← → Previous day │
│ K / → → Next day │
│ D → Day view │
│ W → Week view │
│ M → Month view │
│ C → Create new event │
│ / → Open search │
│ ? → Show keyboard shortcuts │
│ │
│ Time Grid Navigation: │
│ ───────────────────── │
│ ↑ / ↓ → Move between time slots (30 min) │
│ Home → Go to start of day (12 AM) │
│ End → Go to end of day (11 PM) │
│ Page Up → Jump 3 hours earlier │
│ Page Down → Jump 3 hours later │
│ Enter → Create event at focused time │
│ Tab → Move to next event │
│ Shift+Tab → Move to previous event │
│ │
│ Event Focus: │
│ ───────────── │
│ Enter / Space → Open event details │
│ E → Edit event │
│ Delete / Backspace → Delete event (with confirmation) │
│ D → Duplicate event │
│ Escape → Close modal / Cancel action │
│ │
│ Drag Mode (with keyboard): │
│ ───────────────────────── │
│ Space (on event) → Enter drag mode │
│ ↑ / ↓ → Move event by 15 minutes │
│ Shift + ↑/↓ → Move event by 1 hour │
│ Enter → Confirm new position │
│ Escape → Cancel drag │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Accessible Event Component
// components/AccessibleEventBlock.jsxconstAccessibleEventBlock=({event,isSelected,onSelect,onEdit,onDelete,onDragStart})=>{consteventRef=useRef(null);const[isDragMode,setIsDragMode]=useState(false);const{announce}=useAriaLive();consthandleKeyDown=(e)=>{switch (e.key){case'Enter':case'':if (isDragMode){// Confirm dragsetIsDragMode(false);announce(`Event moved to ${formatTime(event.start)}`);}else{e.preventDefault();onSelect(event);}break;case'e':e.preventDefault();onEdit(event);break;case'Delete':case'Backspace':e.preventDefault();if (confirm('Delete this event?')){onDelete(event);announce('Event deleted');}break;case'ArrowUp':case'ArrowDown':if (isDragMode){e.preventDefault();constdelta=e.shiftKey?60:15;// minutesconstdirection=e.key==='ArrowUp'?-1:1;moveEvent(event,delta*direction);announce(`Moved to ${formatTime(event.start)}`);}break;case'Escape':if (isDragMode){setIsDragMode(false);announce('Drag cancelled');}break;}};// Format for screen readerconstariaLabel=useMemo(()=>{consttimeRange=`${formatTime(event.start)} to ${formatTime(event.end)}`;constcalendar=event.calendarName;constduration=formatDuration(event.end-event.start);return`${event.title}, ${timeRange}, ${duration}, ${calendar} calendar`;},[event]);return (<divref={eventRef}role="button"tabIndex={0}aria-label={ariaLabel}aria-selected={isSelected}aria-describedby={`event-${event.id}-details`}className={`event-block ${isDragMode?'event-block--dragging':''}`}style={{backgroundColor:event.color,top:calculateTop(event.start),height:calculateHeight(event.start,event.end)}}onClick={()=>onSelect(event)}onKeyDown={handleKeyDown}onMouseDown={(e)=>{if (e.button===0)onDragStart(event,e);}}><divclassName="event-block__title">{event.title}</div><divclassName="event-block__time">{formatTimeRange(event.start,event.end)}</div>{/* Hidden details for screen readers */}<divid={`event-${event.id}-details`}className="sr-only">{event.location&&`Location: ${event.location}.`}{event.description&&`Description: ${event.description}.`}
Press E to edit, Delete to remove.
</div></div>);};// ARIA Live region hookconstuseAriaLive=()=>{constannounce=useCallback((message)=>{constliveRegion=document.getElementById('aria-live-region');if (liveRegion){liveRegion.textContent=message;// Clear after announcementsetTimeout(()=>{liveRegion.textContent='';},1000);}},[]);return{announce};};
// hooks/useTouchDrag.jsconstuseTouchDrag=({onDragStart,onDragMove,onDragEnd})=>{const[isDragging,setIsDragging]=useState(false);constlongPressTimer=useRef(null);conststartPosition=useRef({x:0,y:0});constcurrentPosition=useRef({x:0,y:0});constLONG_PRESS_DURATION=500;constMOVE_THRESHOLD=10;consthandleTouchStart=useCallback((e,event)=>{consttouch=e.touches[0];startPosition.current={x:touch.clientX,y:touch.clientY};// Start long press timerlongPressTimer.current=setTimeout(()=>{// Haptic feedbackif (navigator.vibrate){navigator.vibrate(50);}setIsDragging(true);onDragStart(event,{x:touch.clientX,y:touch.clientY});},LONG_PRESS_DURATION);},[onDragStart]);consthandleTouchMove=useCallback((e)=>{consttouch=e.touches[0];currentPosition.current={x:touch.clientX,y:touch.clientY};// Cancel long press if moved too muchif (!isDragging&&longPressTimer.current){constdx=Math.abs(touch.clientX-startPosition.current.x);constdy=Math.abs(touch.clientY-startPosition.current.y);if (dx>MOVE_THRESHOLD||dy>MOVE_THRESHOLD){clearTimeout(longPressTimer.current);longPressTimer.current=null;}}// Handle drag moveif (isDragging){e.preventDefault();// Prevent scrollonDragMove({x:touch.clientX,y:touch.clientY,deltaY:touch.clientY-startPosition.current.y});}},[isDragging,onDragMove]);consthandleTouchEnd=useCallback(()=>{clearTimeout(longPressTimer.current);longPressTimer.current=null;if (isDragging){setIsDragging(false);onDragEnd(currentPosition.current);}},[isDragging,onDragEnd]);return{isDragging,handlers:{onTouchStart:handleTouchStart,onTouchMove:handleTouchMove,onTouchEnd:handleTouchEnd,onTouchCancel:handleTouchEnd}};};
// __tests__/utils/timeCalculations.test.jsdescribe('Time Calculations',()=>{describe('calculateEventPosition',()=>{it('calculates correct top position for 9 AM event',()=>{constevent={start:newDate('2024-12-22T09:00:00'),end:newDate('2024-12-22T10:00:00')};constgridConfig={startHour:0,slotHeight:48};// 48px per 30minconstposition=calculateEventPosition(event,gridConfig);expect(position.top).toBe(9*2*48);// 9 hours * 2 slots/hour * 48pxexpect(position.height).toBe(2*48);// 1 hour = 2 slots});it('handles events crossing midnight',()=>{constevent={start:newDate('2024-12-22T23:00:00'),end:newDate('2024-12-23T01:00:00')};constposition=calculateEventPosition(event,{forDate:'2024-12-22'});// Should only show portion within the dayexpect(position.height).toBe(2*48);// 11 PM to midnight});});describe('snapToInterval',()=>{it('snaps to 15-minute intervals',()=>{expect(snapToInterval(newDate('2024-12-22T09:07:00'),15)).toEqual(newDate('2024-12-22T09:00:00'));expect(snapToInterval(newDate('2024-12-22T09:08:00'),15)).toEqual(newDate('2024-12-22T09:15:00'));});});});// __tests__/utils/overlap.test.jsdescribe('Overlap Algorithm',()=>{it('calculates columns for non-overlapping events',()=>{constevents=[{id:'1',start:'09:00',end:'10:00'},{id:'2',start:'10:00',end:'11:00'},{id:'3',start:'11:00',end:'12:00'}];constlayout=calculateOverlapLayout(events);// Each event gets full widthexpect(layout['1']).toEqual({column:0,totalColumns:1});expect(layout['2']).toEqual({column:0,totalColumns:1});expect(layout['3']).toEqual({column:0,totalColumns:1});});it('splits width for overlapping events',()=>{constevents=[{id:'1',start:'09:00',end:'11:00'},{id:'2',start:'10:00',end:'12:00'}];constlayout=calculateOverlapLayout(events);expect(layout['1']).toEqual({column:0,totalColumns:2});expect(layout['2']).toEqual({column:1,totalColumns:2});});it('handles three overlapping events',()=>{constevents=[{id:'1',start:'09:00',end:'12:00'},{id:'2',start:'10:00',end:'11:00'},{id:'3',start:'10:30',end:'11:30'}];constlayout=calculateOverlapLayout(events);expect(layout['1'].totalColumns).toBe(3);expect(layout['2'].totalColumns).toBe(3);expect(layout['3'].totalColumns).toBe(3);});});// __tests__/utils/recurring.test.jsdescribe('Recurring Events',()=>{it('expands daily recurrence within range',()=>{constevent={id:'master-1',title:'Daily Standup',start:'2024-01-01T09:00:00Z',end:'2024-01-01T09:30:00Z',rrule:'FREQ=DAILY'};constinstances=expandRecurring(event,{start:'2024-01-01',end:'2024-01-07'});expect(instances).toHaveLength(7);expect(instances[0].start).toBe('2024-01-01T09:00:00Z');expect(instances[6].start).toBe('2024-01-07T09:00:00Z');});it('skips exceptions',()=>{constevent={id:'master-1',rrule:'FREQ=DAILY',exceptions:[{originalStart:'2024-01-03T09:00:00Z',status:'cancelled'}]};constinstances=expandRecurring(event,{start:'2024-01-01',end:'2024-01-05'});expect(instances).toHaveLength(4);// Jan 3 is skipped});});
Integration Tests
// __tests__/integration/eventCRUD.test.jsimport{renderHook,act,waitFor}from'@testing-library/react';import{QueryClient,QueryClientProvider}from'@tanstack/react-query';import{rest}from'msw';import{setupServer}from'msw/node';import{useEvents,useCreateEvent}from'../hooks/useEvents';constserver=setupServer(rest.get('/api/events',(req,res,ctx)=>{returnres(ctx.json({events:[{id:'evt-1',title:'Team Meeting',start:'2024-12-22T10:00:00Z',end:'2024-12-22T11:00:00Z'}]}));}),rest.post('/api/events',async (req,res,ctx)=>{constbody=awaitreq.json();returnres(ctx.json({id:'evt-new',...body}));}));beforeAll(()=>server.listen());afterEach(()=>server.resetHandlers());afterAll(()=>server.close());describe('Event CRUD Operations',()=>{constqueryClient=newQueryClient({defaultOptions:{queries:{retry:false}}});constwrapper=({children})=>(<QueryClientProviderclient={queryClient}>{children}</QueryClientProvider>);it('fetches events for a date',async ()=>{const{result}=renderHook(()=>useEvents({date:'2024-12-22'}),{wrapper});awaitwaitFor(()=>{expect(result.current.isSuccess).toBe(true);});expect(result.current.data.events).toHaveLength(1);expect(result.current.data.events[0].title).toBe('Team Meeting');});it('creates event with optimistic update',async ()=>{const{result:eventsResult}=renderHook(()=>useEvents({date:'2024-12-22'}),{wrapper});const{result:createResult}=renderHook(()=>useCreateEvent(),{wrapper});awaitwaitFor(()=>eventsResult.current.isSuccess);act(()=>{createResult.current.mutate({title:'New Event',start:'2024-12-22T14:00:00Z',end:'2024-12-22T15:00:00Z'});});// Optimistic update should show immediatelyawaitwaitFor(()=>{expect(eventsResult.current.data.events).toHaveLength(2);});});});
E2E Tests
// e2e/calendar.spec.ts (Playwright)import{test,expect}from'@playwright/test';test.describe('Calendar Day View',()=>{test.beforeEach(async ({page})=>{awaitpage.goto('/calendar/2024-12-22');});test('creates event by clicking on time slot',async ({page})=>{// Click on 10 AM slotawaitpage.click('[data-time="10:00"]');// Modal should openawaitexpect(page.locator('[data-testid="event-modal"]')).toBeVisible();// Fill in detailsawaitpage.fill('[name="title"]','E2E Test Event');awaitpage.click('button[type="submit"]');// Event should appear in gridawaitexpect(page.locator('.event-block:has-text("E2E Test Event")')).toBeVisible();});test('drags event to new time',async ({page})=>{// Create an event firstawaitpage.evaluate(()=>{window.__TEST_EVENTS__=[{id:'test-1',title:'Drag Me',start:'2024-12-22T09:00:00Z',end:'2024-12-22T10:00:00Z'}];});awaitpage.reload();// Get event and target positionsconstevent=page.locator('.event-block:has-text("Drag Me")');consttargetSlot=page.locator('[data-time="14:00"]');// Drag and dropawaitevent.dragTo(targetSlot);// Verify new positionawaitexpect(event).toHaveCSS('top',/^(?!0px)/);// Changed from original// Verify API callconstrequest=awaitpage.waitForRequest(req=>req.method()==='PATCH'&&req.url().includes('/events/'));constbody=request.postDataJSON();expect(body.start).toContain('14:00');});test('keyboard navigation works',async ({page})=>{// Focus time gridawaitpage.keyboard.press('Tab');// Navigate downawaitpage.keyboard.press('ArrowDown');awaitpage.keyboard.press('ArrowDown');// Create event with Enterawaitpage.keyboard.press('Enter');awaitexpect(page.locator('[data-testid="event-modal"]')).toBeVisible();});test('handles timezone display correctly',async ({page})=>{// Set timezone preferenceawaitpage.evaluate(()=>{localStorage.setItem('timezone','America/New_York');});awaitpage.reload();// Event at 15:00 UTC should show as 10:00 AM ESTawaitexpect(page.locator('.event-block:has-text("10:00 AM")')).toBeVisible();});});
15. Offline Support & PWA
Service Worker Strategy
┌─────────────────────────────────────────────────────────────────────────────┐
│ OFFLINE CALENDAR ARCHITECTURE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Caching Strategies: │
│ ─────────────────── │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ASSET TYPE │ STRATEGY │ CACHE │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ App Shell │ Cache First │ app-shell-v2 │ │
│ │ (HTML, JS, CSS) │ (pre-cached) │ │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ Events Data │ Network First │ events-cache │ │
│ │ (API responses) │ (fallback to cache) │ TTL: 1 hour │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ Calendar Settings │ Stale While │ settings-cache │ │
│ │ (user prefs) │ Revalidate │ TTL: 24 hours │ │
│ ├─────────────────────────────────────────────────────────────────────┤ │
│ │ Static Images │ Cache First │ image-cache │ │
│ │ (avatars, icons) │ │ TTL: 7 days │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ Offline Capabilities: │
│ ───────────────────── │
│ • View cached events for past 30 days, future 90 days │
│ • Create new events (queued for sync) │
│ • Edit existing events (queued for sync) │
│ • Delete events (queued for sync) │
│ • Full UI functionality │
│ │
│ Sync Strategy: │
│ ────────────── │
│ • Queue mutations in IndexedDB │
│ • Use Background Sync API when available │
│ • Fall back to sync on page load │
│ • Handle conflicts (last-write-wins or prompt) │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
IndexedDB Schema
// db/CalendarDatabase.jsimportDexiefrom'dexie';classCalendarDatabaseextendsDexie{constructor(){super('CalendarApp');this.version(1).stores({// Events tableevents:`
id,
calendarId,
startDate,
[calendarId+startDate],
updatedAt,
syncStatus
`,// Calendars tablecalendars:`
id,
ownerId
`,// Pending changes (sync queue)pendingChanges:`
++id,
type,
entityType,
entityId,
data,
createdAt
`,// Sync metadatasyncState:`
key
`});}// Store events for a date rangeasynccacheEvents(events,dateRange){awaitthis.transaction('rw',this.events,async ()=>{for (consteventofevents){awaitthis.events.put({...event,syncStatus:'synced',startDate:event.start.split('T')[0]});}});// Store sync timestampawaitthis.syncState.put({key:`events:${dateRange.start}:${dateRange.end}`,timestamp:Date.now()});}// Get events for dateasyncgetEventsForDate(date,calendarIds){returnthis.events.where('[calendarId+startDate]').anyOf(calendarIds.map(id=>[id,date])).toArray();}// Queue a change for syncasyncqueueChange(type,entityType,entityId,data){awaitthis.pendingChanges.add({type,// 'create', 'update', 'delete'entityType,// 'event', 'calendar'entityId,data,createdAt:Date.now()});}// Get all pending changesasyncgetPendingChanges(){returnthis.pendingChanges.orderBy('createdAt').toArray();}// Clear a pending change after syncasyncclearPendingChange(id){awaitthis.pendingChanges.delete(id);}}exportconstdb=newCalendarDatabase();
Background Sync
// service-worker.jsimport{BackgroundSyncPlugin}from'workbox-background-sync';import{registerRoute}from'workbox-routing';import{NetworkOnly,NetworkFirst}from'workbox-strategies';// Background sync for event mutationsconsteventSyncQueue=newBackgroundSyncPlugin('event-sync-queue',{maxRetentionTime:24*60// 24 hours});// POST/PATCH/DELETE events go through background syncregisterRoute(({url,request})=>url.pathname.startsWith('/api/events')&&['POST','PATCH','DELETE'].includes(request.method),newNetworkOnly({plugins:[eventSyncQueue]}),'POST');// Sync pending changes when back onlineself.addEventListener('sync',(event)=>{if (event.tag==='sync-calendar-changes'){event.waitUntil(syncPendingChanges());}});asyncfunctionsyncPendingChanges(){constdb=awaitopenDatabase();constpendingChanges=awaitdb.getAll('pendingChanges');for (constchangeofpendingChanges){try{awaitsyncChange(change);awaitdb.delete('pendingChanges',change.id);}catch (error){if (error.status===409){// Conflict - notify userawaitnotifyConflict(change);}// Other errors will be retriedthrowerror;}}}
// components/DayView.jsx with RTL supportconstDayView=()=>{const{locale,direction}=useLocale();return (<divclassName="day-view"dir={direction}// 'ltr' or 'rtl'style={{// CSS logical properties handle RTL automaticallypaddingInlineStart:'60px',// Time column width}}><TimeColumn/><EventsGrid/></div>);};// CSS with logical propertiesconststyles=`
.day-view {
display: flex;
}
.time-column {
/* Instead of 'left', use 'inset-inline-start' */
position: sticky;
inset-inline-start: 0;
/* Instead of 'text-align: right', use 'text-align: end' */
text-align: end;
/* Instead of 'padding-right', use 'padding-inline-end' */
padding-inline-end: 8px;
}
.event-block {
/* Instead of 'margin-left', use 'margin-inline-start' */
margin-inline-start: 4px;
/* Border on the start side (left in LTR, right in RTL) */
border-inline-start: 4px solid var(--calendar-color);
}
/* Navigation arrows flip in RTL */
.nav-prev {
/* Arrow pointing to inline-start */
transform: scaleX(var(--rtl-flip, 1));
}
[dir="rtl"] {
--rtl-flip: -1;
}
`;
i18n Setup
// i18n/config.jsimporti18nfrom'i18next';import{initReactI18next}from'react-i18next';constresources={en:{calendar:{today:'Today',day:'Day',week:'Week',month:'Month',createEvent:'Create event',allDay:'All day',noTitle:'(No title)',moreEvents:'{{count}} more',recurring:{daily:'Daily',weekly:'Weekly on {{day}}',monthly:'Monthly on the {{ordinal}}',yearly:'Annually on {{date}}'}}},es:{calendar:{today:'Hoy',day:'Día',week:'Semana',month:'Mes',createEvent:'Crear evento',allDay:'Todo el día',noTitle:'(Sin título)',moreEvents:'{{count}} más'}},ar:{calendar:{today:'اليوم',day:'يوم',week:'أسبوع',month:'شهر',createEvent:'إنشاء حدث',allDay:'طوال اليوم',noTitle:'(بدون عنوان)',moreEvents:'{{count}} المزيد'}},ja:{calendar:{today:'今日',day:'日',week:'週',month:'月',createEvent:'予定を作成',allDay:'終日',noTitle:'(タイトルなし)',moreEvents:'他 {{count}} 件'}}};i18n.use(initReactI18next).init({resources,lng:navigator.language,fallbackLng:'en',interpolation:{escapeValue:false}});// Set direction based on languageconstrtlLanguages=['ar','he','fa','ur'];i18n.on('languageChanged',(lng)=>{document.documentElement.dir=rtlLanguages.includes(lng)?'rtl':'ltr';document.documentElement.lang=lng;});
// notifications/ReminderNotification.jsclassReminderNotificationManager{asyncrequestPermission(){constpermission=awaitNotification.requestPermission();if (permission==='granted'){awaitthis.subscribeToPush();}returnpermission;}asyncsubscribeToPush(){constregistration=awaitnavigator.serviceWorker.ready;constsubscription=awaitregistration.pushManager.subscribe({userVisibleOnly:true,applicationServerKey:urlBase64ToUint8Array(VAPID_PUBLIC_KEY)});// Send subscription to serverawaitfetch('/api/push/subscribe',{method:'POST',body:JSON.stringify(subscription),headers:{'Content-Type':'application/json'}});}// Show local notification (when tab is open)showLocalNotification(event,minutesBefore){if (Notification.permission!=='granted')return;constnotification=newNotification(event.title,{body:`Starting in ${minutesBefore} minutes`,icon:'/icons/calendar-192.png',tag:`reminder-${event.id}`,data:{eventId:event.id},requireInteraction:true,actions:[{action:'join',title:'Join Meeting'},{action:'snooze',title:'Snooze 5 min'},{action:'dismiss',title:'Dismiss'}]});notification.onclick=()=>{window.focus();navigateToEvent(event.id);notification.close();};}}// Service Worker handlerself.addEventListener('push',(event)=>{constdata=event.data?.json()||{};event.waitUntil(self.registration.showNotification(data.title,{body:data.body,icon:'/icons/calendar-192.png',badge:'/icons/calendar-badge.png',tag:`event-${data.eventId}`,data:data,actions:[{action:'join',title:'Join'},{action:'snooze',title:'Snooze'}]}));});self.addEventListener('notificationclick',(event)=>{event.notification.close();if (event.action==='join'&&event.notification.data.meetingUrl){event.waitUntil(clients.openWindow(event.notification.data.meetingUrl));}elseif (event.action==='snooze'){// Reschedule notification for 5 minutes laterevent.waitUntil(scheduleSnooze(event.notification.data.eventId,5));}else{// Open calendar appevent.waitUntil(clients.openWindow(`/calendar/event/${event.notification.data.eventId}`));}});
// components/VirtualMonthView.jsximport{useVirtualizer}from'@tanstack/react-virtual';constVirtualMonthView=({events,onLoadMore})=>{constparentRef=useRef(null);// Generate weeks for the yearconstweeks=useMemo(()=>{returngenerateWeeksForYear(2024);// [{start, end}, ...]},[]);constvirtualizer=useVirtualizer({count:weeks.length,getScrollElement:()=>parentRef.current,estimateSize:()=>120,// Estimated row heightoverscan:3// Render 3 extra rows above/below});constvirtualRows=virtualizer.getVirtualItems();// Fetch events for visible weeksuseEffect(()=>{constvisibleWeeks=virtualRows.map(row=>weeks[row.index]);constdateRange={start:visibleWeeks[0]?.start,end:visibleWeeks[visibleWeeks.length-1]?.end};if (dateRange.start&&dateRange.end){prefetchEvents(dateRange);}},[virtualRows,weeks]);return (<divref={parentRef}className="month-view-container"style={{height:'100%',overflow:'auto'}}><divstyle={{height:`${virtualizer.getTotalSize()}px`,width:'100%',position:'relative'}}>{virtualRows.map((virtualRow)=>{constweek=weeks[virtualRow.index];constweekEvents=events.filter(e=>isWithinWeek(e,week));return (<divkey={virtualRow.key}style={{position:'absolute',top:0,left:0,width:'100%',height:`${virtualRow.size}px`,transform:`translateY(${virtualRow.start}px)`}}><WeekRowweek={week}events={weekEvents}/></div>);})}</div></div>);};
Lazy Loading Events
// hooks/useLazyEvents.jsconstuseLazyEvents=(visibleRange)=>{constqueryClient=useQueryClient();const[loadedRanges,setLoadedRanges]=useState([]);// Check if range is already loadedconstisRangeLoaded=(range)=>{returnloadedRanges.some(loaded=>loaded.start<=range.start&&loaded.end>=range.end);};// Fetch events for new rangeconst{data,isFetching}=useQuery({queryKey:['events',visibleRange.start,visibleRange.end],queryFn:async ()=>{constresponse=awaitfetch(`/api/events?start=${visibleRange.start}&end=${visibleRange.end}`);returnresponse.json();},enabled:!isRangeLoaded(visibleRange),onSuccess:()=>{setLoadedRanges(prev=>mergeRanges([...prev,visibleRange]));}});// Prefetch adjacent rangesuseEffect(()=>{constnextRange=getNextWeekRange(visibleRange);constprevRange=getPrevWeekRange(visibleRange);queryClient.prefetchQuery({queryKey:['events',nextRange.start,nextRange.end],queryFn:()=>fetchEvents(nextRange)});queryClient.prefetchQuery({queryKey:['events',prevRange.start,prevRange.end],queryFn:()=>fetchEvents(prevRange)});},[visibleRange,queryClient]);// Aggregate all cached eventsconstallEvents=useMemo(()=>{constcached=queryClient.getQueriesData(['events']);returncached.flatMap(([,data])=>data?.events||[]);},[data,queryClient]);return{events:allEvents,isLoading:isFetching};};// Merge overlapping rangesconstmergeRanges=(ranges)=>{if (ranges.length===0)return[];constsorted=ranges.sort((a,b)=>newDate(a.start)-newDate(b.start));constmerged=[sorted[0]];for (leti=1;i<sorted.length;i++){constlast=merged[merged.length-1];constcurrent=sorted[i];if (newDate(current.start)<=newDate(last.end)){last.end=newDate(last.end)>newDate(current.end)?last.end:current.end;}else{merged.push(current);}}returnmerged;};
Quick Reference Cheat Sheet
┌─────────────────────────────────────────────────────────────────────────────┐
│ QUICK REFERENCE │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Protocol Choice: │
│ • Data fetch → REST │
│ • Real-time sync → WebSocket │
│ • NOT: Long polling (too much overhead for calendar) │
│ │
│ Database Choice: │
│ • Events, Calendars, Users → PostgreSQL (ACID, range queries) │
│ • Cache, Pub/Sub → Redis │
│ • Analytics → Cassandra (optional) │
│ │
│ Recurring Events: │
│ • Store RRULE, expand on read │
│ • Never materialize infinite series │
│ • Exceptions stored separately │
│ │
│ Performance: │
│ • Memoize event components │
│ • Use transforms for drag │
│ • throttle with rAF │
│ • CSS containment │
│ │
│ State Management: │
│ • Server state → React Query │
│ • URL state → Router (date, view) │
│ • UI state → Zustand/Context │
│ • Drag state → Local useState │
│ │
│ Time Zones: │
│ • Store UTC, display local │
│ • All-day = date only │
│ • Use date-fns-tz │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Top comments (0)
Subscribe
For further actions, you may consider blocking this person and/or reporting abuse
We're a place where coders share, stay up-to-date and grow their careers.
Top comments (0)