We use cookies to enhance your experience on the site
CodeWorlds

Final Project: Interactive Jurassic Park Control Panel

"This is the moment of truth," announces Ray Arnold, chief systems engineer of Jurassic Park, pointing at the dark, disabled screen of the central control panel. "Our main monitoring system failed during the last storm. You need to rebuild the interactive control panel from scratch, using everything you've learned about events and interactions in JavaScript."

In this project, you'll create a complete, interactive Jurassic Park control panel. The panel will handle clicking, dragging, keyboard events, forms, event delegation, propagation, throttling, custom events, and proper memory management. This is a real test of all skills from this module - from basic event listeners to advanced architectural patterns.

Project Goal

You'll build a control panel with the following features:

  • Park map with clickable sectors (event delegation)
  • Alarm system responding to keyboard shortcuts
  • Incident registration form with validation
  • Dinosaur activity monitor with throttling
  • Event Bus for inter-module communication
  • Proper memory cleanup when closing panels

Functional Requirements

1. Park Map with Event Delegation and Propagation

Instead of adding an event listener to each sector individually, use event delegation - one listener on the map container handles clicks in all sectors. Also remember to control propagation so that clicking a dinosaur doesn't simultaneously activate the sector.

1// HTML structure of the park map
2// <div id="park-map">
3//   <div class="sector" data-sector="A" data-danger="high">
4//     <span class="dino" data-dino-id="1">T-Rex</span>
5//     <span class="dino" data-dino-id="2">Velociraptor</span>
6//   </div>
7//   <div class="sector" data-sector="B" data-danger="low">
8//     <span class="dino" data-dino-id="3">Triceratops</span>
9//   </div>
10// </div>
11
12class ParkMap {
13  constructor(mapElement) {
14    this.mapElement = mapElement;
15    this.selectedSector = null;
16    this.setupDelegation();
17  }
18
19  setupDelegation() {
20    // One listener handles all clicks on the map
21    this.mapElement.addEventListener('click', (event) => {
22      const dinoElement = event.target.closest('.dino');
23      
24      if (dinoElement) {
25        // Clicked on a dinosaur - stop propagation to sector
26        event.stopPropagation();
27        this.handleDinoClick(dinoElement);
28        return;
29      }
30
31      const sectorElement = event.target.closest('.sector');
32      if (sectorElement) {
33        this.handleSectorClick(sectorElement);
34      }
35    });
36  }
37
38  handleSectorClick(sectorEl) {
39    // Deselect previous sector
40    if (this.selectedSector) {
41      this.selectedSector.classList.remove('selected');
42    }
43    
44    sectorEl.classList.add('selected');
45    this.selectedSector = sectorEl;
46
47    const sectorId = sectorEl.dataset.sector;
48    const danger = sectorEl.dataset.danger;
49
50    console.log(`Sector ${sectorId} selected [Danger: ${danger}]`);
51
52    // Emit custom event with selected sector info
53    const sectorEvent = new CustomEvent('sector:selected', {
54      detail: { sectorId, danger },
55      bubbles: true
56    });
57    sectorEl.dispatchEvent(sectorEvent);
58  }
59
60  handleDinoClick(dinoEl) {
61    const dinoId = dinoEl.dataset.dinoId;
62    const dinoName = dinoEl.textContent;
63    
64    console.log(`Selected dinosaur: ${dinoName} (ID: ${dinoId})`);
65
66    // Custom event with dinosaur data
67    const dinoEvent = new CustomEvent('dino:selected', {
68      detail: { dinoId, dinoName },
69      bubbles: true
70    });
71    dinoEl.dispatchEvent(dinoEvent);
72  }
73
74  // Dynamically adding new sectors - thanks to delegation
75  // no need to add new listeners
76  addSector(sectorId, danger) {
77    const sector = document.createElement('div');
78    sector.className = 'sector';
79    sector.dataset.sector = sectorId;
80    sector.dataset.danger = danger;
81    sector.textContent = `Sector ${sectorId}`;
82    this.mapElement.appendChild(sector);
83    // Event listener already works automatically!
84  }
85}

2. Alarm System with Keyboard and Mouse Events

Create a system that responds to keyboard shortcuts for quickly launching emergency procedures. The system must handle key combinations, distinguish context (is the user typing in a form or controlling the panel), and respond to different types of mouse events.

1class AlarmSystem {
2  constructor() {
3    this.alarmLevel = 0; // 0-4 (0 = none, 4 = critical)
4    this.isActive = true;
5    this.keyHandlers = new Map();
6    this.boundHandleKeyDown = this.handleKeyDown.bind(this);
7    this.boundHandleKeyUp = this.handleKeyUp.bind(this);
8    this.pressedKeys = new Set();
9
10    this.registerShortcuts();
11    this.attachListeners();
12  }
13
14  registerShortcuts() {
15    // Ctrl+1 to Ctrl+4: Alarm levels
16    this.keyHandlers.set('ctrl+1', () => this.setAlarmLevel(1));
17    this.keyHandlers.set('ctrl+2', () => this.setAlarmLevel(2));
18    this.keyHandlers.set('ctrl+3', () => this.setAlarmLevel(3));
19    this.keyHandlers.set('ctrl+4', () => this.setAlarmLevel(4));
20    
21    // Escape: Disable alarm
22    this.keyHandlers.set('escape', () => this.setAlarmLevel(0));
23    
24    // Ctrl+Shift+E: Evacuation
25    this.keyHandlers.set('ctrl+shift+e', () => this.triggerEvacuation());
26    
27    // Ctrl+Shift+L: Lock all enclosures
28    this.keyHandlers.set('ctrl+shift+l', () => this.lockAllEnclosures());
29  }
30
31  attachListeners() {
32    document.addEventListener('keydown', this.boundHandleKeyDown);
33    document.addEventListener('keyup', this.boundHandleKeyUp);
34  }
35
36  handleKeyDown(event) {
37    // Ignore shortcuts when user is typing in a form
38    if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
39      return;
40    }
41
42    this.pressedKeys.add(event.key.toLowerCase());
43
44    // Build key combination
45    const combo = this.buildKeyCombo(event);
46    
47    const handler = this.keyHandlers.get(combo);
48    if (handler) {
49      event.preventDefault(); // Prevent default browser action
50      handler();
51    }
52  }
53
54  handleKeyUp(event) {
55    this.pressedKeys.delete(event.key.toLowerCase());
56  }
57
58  buildKeyCombo(event) {
59    const parts = [];
60    if (event.ctrlKey || event.metaKey) parts.push('ctrl');
61    if (event.shiftKey) parts.push('shift');
62    if (event.altKey) parts.push('alt');
63    
64    const key = event.key.toLowerCase();
65    if (!['control', 'shift', 'alt', 'meta'].includes(key)) {
66      parts.push(key);
67    }
68    
69    return parts.join('+');
70  }
71
72  setAlarmLevel(level) {
73    const previousLevel = this.alarmLevel;
74    this.alarmLevel = level;
75    
76    console.log(`⚠️ Alarm level: ${previousLevel}${level}`);
77
78    // Emit custom event about alarm level change
79    document.dispatchEvent(new CustomEvent('alarm:levelChanged', {
80      detail: { previousLevel, currentLevel: level }
81    }));
82  }
83
84  triggerEvacuation() {
85    console.log('🚨 EVACUATION TRIGGERED!');
86    this.setAlarmLevel(4);
87    document.dispatchEvent(new CustomEvent('alarm:evacuation'));
88  }
89
90  lockAllEnclosures() {
91    console.log('🔒 Locking all enclosures...');
92    document.dispatchEvent(new CustomEvent('alarm:lockdown'));
93  }
94
95  // Memory cleanup - CRITICAL
96  destroy() {
97    document.removeEventListener('keydown', this.boundHandleKeyDown);
98    document.removeEventListener('keyup', this.boundHandleKeyUp);
99    this.keyHandlers.clear();
100    this.pressedKeys.clear();
101    this.isActive = false;
102    console.log('AlarmSystem: Listeners removed, memory cleaned');
103  }
104}

3. Incident Registration Form with Validation

Build a form handling

submit
,
input
,
change
,
focus
, and
blur
events. The form should validate data in real time and prevent submission of invalid data.

1class IncidentForm {
2  constructor(formElement) {
3    this.form = formElement;
4    this.errors = new Map();
5    this.boundHandlers = [];
6
7    this.setupValidation();
8    this.setupSubmit();
9  }
10
11  setupValidation() {
12    // Validate "description" field on every typed character
13    const descriptionInput = this.form.querySelector('#description');
14    const descHandler = (event) => {
15      const value = event.target.value;
16      if (value.length < 10) {
17        this.setError('description', 'Description must be at least 10 characters');
18      } else if (value.length > 500) {
19        this.setError('description', 'Description cannot exceed 500 characters');
20      } else {
21        this.clearError('description');
22      }
23    };
24    descriptionInput.addEventListener('input', descHandler);
25    this.boundHandlers.push({ element: descriptionInput, type: 'input', handler: descHandler });
26
27    // Validate "sector" select on change
28    const sectorSelect = this.form.querySelector('#sector');
29    const sectorHandler = (event) => {
30      if (!event.target.value) {
31        this.setError('sector', 'Select a sector');
32      } else {
33        this.clearError('sector');
34      }
35    };
36    sectorSelect.addEventListener('change', sectorHandler);
37    this.boundHandlers.push({ element: sectorSelect, type: 'change', handler: sectorHandler });
38
39    // Focus/blur visual effects
40    const allInputs = this.form.querySelectorAll('input, select, textarea');
41    allInputs.forEach(input => {
42      const focusHandler = () => input.parentElement.classList.add('focused');
43      const blurHandler = () => {
44        input.parentElement.classList.remove('focused');
45        // Validate field after leaving
46        input.dispatchEvent(new Event('input'));
47      };
48      
49      input.addEventListener('focus', focusHandler);
50      input.addEventListener('blur', blurHandler);
51      this.boundHandlers.push(
52        { element: input, type: 'focus', handler: focusHandler },
53        { element: input, type: 'blur', handler: blurHandler }
54      );
55    });
56  }
57
58  setupSubmit() {
59    const submitHandler = (event) => {
60      event.preventDefault(); // Prevent default form submission
61
62      if (this.errors.size > 0) {
63        console.log('Form contains errors:', [...this.errors.values()]);
64        this.highlightErrors();
65        return;
66      }
67
68      const formData = new FormData(this.form);
69      const incident = Object.fromEntries(formData.entries());
70      
71      console.log('Incident reported:', incident);
72
73      // Custom event with incident data
74      document.dispatchEvent(new CustomEvent('incident:reported', {
75        detail: incident
76      }));
77
78      this.form.reset();
79    };
80
81    this.form.addEventListener('submit', submitHandler);
82    this.boundHandlers.push({ element: this.form, type: 'submit', handler: submitHandler });
83  }
84
85  setError(field, message) {
86    this.errors.set(field, message);
87    const errorEl = this.form.querySelector(`#${field}-error`);
88    if (errorEl) errorEl.textContent = message;
89  }
90
91  clearError(field) {
92    this.errors.delete(field);
93    const errorEl = this.form.querySelector(`#${field}-error`);
94    if (errorEl) errorEl.textContent = '';
95  }
96
97  highlightErrors() {
98    this.errors.forEach((message, field) => {
99      const input = this.form.querySelector(`#${field}`);
100      if (input) {
101        input.classList.add('error');
102        setTimeout(() => input.classList.remove('error'), 2000);
103      }
104    });
105  }
106
107  // Cleanup of ALL listeners
108  destroy() {
109    this.boundHandlers.forEach(({ element, type, handler }) => {
110      element.removeEventListener(type, handler);
111    });
112    this.boundHandlers = [];
113    this.errors.clear();
114    console.log('IncidentForm: All listeners removed');
115  }
116}

4. Activity Monitor with Throttling and Event Bus

Create a dinosaur activity monitoring system that uses throttling to limit update frequency and Event Bus for communication between all panel modules.

1// Event Bus - central communication system
2class EventBus {
3  constructor() {
4    this.listeners = new Map();
5  }
6
7  on(event, callback) {
8    if (!this.listeners.has(event)) {
9      this.listeners.set(event, new Set());
10    }
11    this.listeners.get(event).add(callback);
12    
13    // Return a function to unsubscribe
14    return () => this.off(event, callback);
15  }
16
17  off(event, callback) {
18    const callbacks = this.listeners.get(event);
19    if (callbacks) {
20      callbacks.delete(callback);
21      if (callbacks.size === 0) {
22        this.listeners.delete(event);
23      }
24    }
25  }
26
27  emit(event, data) {
28    const callbacks = this.listeners.get(event);
29    if (callbacks) {
30      callbacks.forEach(cb => {
31        try {
32          cb(data);
33        } catch (error) {
34          console.error(`Error in listener "${event}":`, error);
35        }
36      });
37    }
38  }
39
40  // Clear all subscriptions
41  clear() {
42    this.listeners.clear();
43    console.log('EventBus: All subscriptions cleared');
44  }
45}
46
47// Throttle utility
48function throttle(fn, delay) {
49  let lastCall = 0;
50  let timeoutId = null;
51
52  const throttled = function(...args) {
53    const now = Date.now();
54    const remaining = delay - (now - lastCall);
55
56    if (remaining <= 0) {
57      if (timeoutId) {
58        clearTimeout(timeoutId);
59        timeoutId = null;
60      }
61      lastCall = now;
62      fn.apply(this, args);
63    } else if (!timeoutId) {
64      timeoutId = setTimeout(() => {
65        lastCall = Date.now();
66        timeoutId = null;
67        fn.apply(this, args);
68      }, remaining);
69    }
70  };
71
72  // Method for cleanup
73  throttled.cancel = () => {
74    if (timeoutId) {
75      clearTimeout(timeoutId);
76      timeoutId = null;
77    }
78  };
79
80  return throttled;
81}
82
83// Activity monitor with throttling
84class ActivityMonitor {
85  constructor(eventBus, displayElement) {
86    this.eventBus = eventBus;
87    this.display = displayElement;
88    this.unsubscribers = [];
89    this.throttledFunctions = [];
90
91    this.setupMonitoring();
92  }
93
94  setupMonitoring() {
95    // Throttled mouse position updates on map
96    const updatePosition = throttle((data) => {
97      this.display.querySelector('.position').textContent =
98        `Cursor: (${data.x}, ${data.y})`;
99    }, 100); // Maximum 10 updates per second
100    this.throttledFunctions.push(updatePosition);
101
102    // Listen for events from EventBus
103    const unsub1 = this.eventBus.on('dino:moved', (data) => {
104      this.logActivity(`Dinosaur ${data.name} moved to (${data.x}, ${data.y})`);
105    });
106
107    const unsub2 = this.eventBus.on('alarm:levelChanged', (data) => {
108      this.logActivity(`Alarm: level ${data.previousLevel}${data.currentLevel}`);
109    });
110
111    const unsub3 = this.eventBus.on('incident:reported', (data) => {
112      this.logActivity(`New incident in sector ${data.sector}: ${data.description}`);
113    });
114
115    this.unsubscribers.push(unsub1, unsub2, unsub3);
116
117    // Mousemove on map with throttling
118    const mapElement = document.getElementById('park-map');
119    if (mapElement) {
120      const mouseMoveHandler = (event) => {
121        const rect = mapElement.getBoundingClientRect();
122        updatePosition({
123          x: Math.round(event.clientX - rect.left),
124          y: Math.round(event.clientY - rect.top)
125        });
126      };
127      mapElement.addEventListener('mousemove', mouseMoveHandler);
128      this.unsubscribers.push(() => {
129        mapElement.removeEventListener('mousemove', mouseMoveHandler);
130      });
131    }
132  }
133
134  logActivity(message) {
135    const entry = document.createElement('div');
136    entry.className = 'log-entry';
137    const time = new Date().toLocaleTimeString();
138    entry.textContent = `[${time}] ${message}`;
139    
140    this.display.querySelector('.log').prepend(entry);
141    
142    // Limit the number of log entries
143    const entries = this.display.querySelectorAll('.log-entry');
144    if (entries.length > 50) {
145      entries[entries.length - 1].remove();
146    }
147  }
148
149  // Cleanup EVERYTHING
150  destroy() {
151    // Unsubscribe from EventBus
152    this.unsubscribers.forEach(unsub => unsub());
153    this.unsubscribers = [];
154
155    // Cancel throttled functions
156    this.throttledFunctions.forEach(fn => fn.cancel());
157    this.throttledFunctions = [];
158
159    console.log('ActivityMonitor: Full cleanup completed');
160  }
161}

Implementation Tips

Start by creating a central

EventBus
that will connect all modules. Then build individual components in order:

1// 1. Initialize Event Bus
2const eventBus = new EventBus();
3
4// 2. Park map with delegation
5const parkMap = new ParkMap(document.getElementById('park-map'));
6
7// 3. Alarm system
8const alarmSystem = new AlarmSystem();
9
10// 4. Incident form
11const incidentForm = new IncidentForm(document.getElementById('incident-form'));
12
13// 5. Activity monitor
14const activityMonitor = new ActivityMonitor(
15  eventBus,
16  document.getElementById('activity-panel')
17);
18
19// 6. Connect modules via EventBus
20// Listen for custom DOM events and redirect to EventBus
21document.addEventListener('sector:selected', (e) => {
22  eventBus.emit('sector:selected', e.detail);
23});
24
25document.addEventListener('alarm:levelChanged', (e) => {
26  eventBus.emit('alarm:levelChanged', e.detail);
27});
28
29document.addEventListener('incident:reported', (e) => {
30  eventBus.emit('incident:reported', e.detail);
31});
32
33// 7. Cleanup on close (e.g., when changing views in SPA)
34function destroyPanel() {
35  parkMap.destroy?.();
36  alarmSystem.destroy();
37  incidentForm.destroy();
38  activityMonitor.destroy();
39  eventBus.clear();
40  console.log('Control panel closed - all resources released');
41}

Remember the key principles:

  • Event delegation instead of many listeners on individual elements
  • event.stopPropagation()
    when a click shouldn't "bubble" higher
  • event.preventDefault()
    for forms and keyboard shortcuts
  • Throttling for high-frequency events (mousemove, scroll)
  • Storing references to handlers so you can remove them later
  • destroy()
    method
    in every module for memory cleanup

Good Luck!

"Jurassic Park systems don't forgive mistakes," says Ray Arnold, lighting another cigarette. "A memory leak in the enclosure monitoring means the system will crash after a few hours. An unremoved event listener can block the garbage collector and consume more and more RAM. And poorly handled event propagation can cause a click on the evacuation button to open the T-Rex enclosure gate instead of closing it."

"Build this panel as if people's lives in the park depend on it. Because in a sense - they do."

Open the editor and create your interactive control panel!

Go to CodeWorlds