smitchel/emberguard-bbq-master
0
1// EmberGuard BBQ Master - Main Application Script2 3// State management4const state = {5 currentTemp: 228,6 targetTemp: 225,7 unit: 'F',8 probes: [9 { id: 1, temp: 165, target: 195, name: 'Brisket', status: 'cooking' },10 { id: 2, temp: 142, target: 165, name: 'Ribs', status: 'smoking' },11 { id: 3, temp: null, target: null, name: null, status: 'available' }12 ],13 cookTime: 9240, // seconds14 fanSpeed: 45,15 fireStatus: 'active',16 batteryLevel: 87,17 isConnected: true,18 chart: null,19 tempHistory: [],20 currentView: 'dashboard'21};22 23// Initialize app24document.addEventListener('DOMContentLoaded', () => {25 initChart();26 startSimulation();27 setupEventListeners();28 renderHistory();29 updateDisplay();30});31 32// Chart.js setup33function initChart() {34 const ctx = document.getElementById('tempChart').getContext('2d');35 36 // Generate initial data37 const now = Date.now();38 for (let i = 60; i >= 0; i--) {39 state.tempHistory.push({40 time: now - i * 60000,41 pit: 220 + Math.random() * 15,42 probe1: 160 + Math.random() * 10,43 probe2: 135 + Math.random() * 1544 });45 }46 47 const gradientPit = ctx.createLinearGradient(0, 0, 0, 300);48 gradientPit.addColorStop(0, 'rgba(249, 115, 22, 0.3)');49 gradientPit.addColorStop(1, 'rgba(249, 115, 22, 0)');50 51 const gradientProbe1 = ctx.createLinearGradient(0, 0, 0, 300);52 gradientProbe1.addColorStop(0, 'rgba(239, 68, 68, 0.2)');53 gradientProbe1.addColorStop(1, 'rgba(239, 68, 68, 0)');54 55 state.chart = new Chart(ctx, {56 type: 'line',57 data: {58 labels: state.tempHistory.map(d => new Date(d.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })),59 datasets: [60 {61 label: 'Pit Temp',62 data: state.tempHistory.map(d => d.pit),63 borderColor: '#f97316',64 backgroundColor: gradientPit,65 fill: true,66 tension: 0.4,67 pointRadius: 0,68 pointHoverRadius: 669 },70 {71 label: 'Probe 1',72 data: state.tempHistory.map(d => d.probe1),73 borderColor: '#ef4444',74 backgroundColor: gradientProbe1,75 fill: true,76 tension: 0.4,77 pointRadius: 0,78 pointHoverRadius: 479 },80 {81 label: 'Probe 2',82 data: state.tempHistory.map(d => d.probe2),83 borderColor: '#3b82f6',84 borderDash: [5, 5],85 fill: false,86 tension: 0.4,87 pointRadius: 0,88 pointHoverRadius: 489 }90 ]91 },92 options: {93 responsive: true,94 maintainAspectRatio: false,95 interaction: {96 mode: 'index',97 intersect: false98 },99 plugins: {100 legend: {101 position: 'top',102 align: 'end',103 labels: {104 usePointStyle: true,105 boxWidth: 8,106 padding: 15,107 color: document.documentElement.classList.contains('dark') ? '#9ca3af' : '#6b7280'108 }109 },110 tooltip: {111 backgroundColor: document.documentElement.classList.contains('dark') ? '#1f2937' : '#ffffff',112 titleColor: document.documentElement.classList.contains('dark') ? '#f3f4f6' : '#111827',113 bodyColor: document.documentElement.classList.contains('dark') ? '#d1d5db' : '#4b5563',114 borderColor: document.documentElement.classList.contains('dark') ? '#374151' : '#e5e7eb',115 borderWidth: 1,116 padding: 12,117 cornerRadius: 8,118 displayColors: true119 }120 },121 scales: {122 x: {123 grid: {124 display: false,125 drawBorder: false126 },127 ticks: {128 color: document.documentElement.classList.contains('dark') ? '#6b7280' : '#9ca3af',129 maxTicksLimit: 8130 }131 },132 y: {133 min: 100,134 max: 400,135 grid: {136 color: document.documentElement.classList.contains('dark') ? '#374151' : '#f3f4f6',137 drawBorder: false138 },139 ticks: {140 color: document.documentElement.classList.contains('dark') ? '#6b7280' : '#9ca3af',141 callback: function(value) {142 return value + '°';143 }144 }145 }146 }147 }148 });149}150 151// Real-time simulation152function startSimulation() {153 setInterval(() => {154 // Simulate temperature fluctuations155 const fluctuation = (Math.random() - 0.5) * 3;156 state.currentTemp = Math.max(150, Math.min(500, state.currentTemp + fluctuation));157 158 // Adjust fan speed based on difference from target159 const diff = state.targetTemp - state.currentTemp;160 state.fanSpeed = Math.max(0, Math.min(100, state.fanSpeed + diff * 0.1 + (Math.random() - 0.5) * 5));161 162 // Update probe temps163 state.probes[0].temp = Math.min(state.probes[0].target, state.probes[0].temp + Math.random() * 0.5);164 state.probes[1].temp = Math.min(state.probes[1].target, state.probes[1].temp + Math.random() * 0.3);165 166 // Increment cook time167 state.cookTime++;168 169 // Add to history170 if (state.cookTime % 60 === 0) {171 state.tempHistory.push({172 time: Date.now(),173 pit: state.currentTemp,174 probe1: state.probes[0].temp,175 probe2: state.probes[1].temp176 });177 if (state.tempHistory.length > 120) state.tempHistory.shift();178 updateChart();179 }180 181 updateDisplay();182 }, 1000);183}184 185function updateChart() {186 if (!state.chart) return;187 188 state.chart.data.labels = state.tempHistory.map(d => new Date(d.time).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));189 state.chart.data.datasets[0].data = state.tempHistory.map(d => d.pit);190 state.chart.data.datasets[1].data = state.tempHistory.map(d => d.probe1);191 state.chart.data.datasets[2].data = state.tempHistory.map(d => d.probe2);192 state.chart.update('none');193}194 195function updateDisplay() {196 // Update main temperature197 const tempEl = document.getElementById('current-temp');198 if (tempEl) {199 tempEl.textContent = Math.round(convertTemp(state.currentTemp));200 }201 202 // Update gauge203 const gauge = document.getElementById('temp-gauge');204 if (gauge) {205 const maxTemp = 500;206 const percentage = Math.min(state.currentTemp / maxTemp, 1);207 const offset = 283 - (283 * percentage);208 gauge.style.strokeDashoffset = offset;209 210 // Color based on temp211 if (state.currentTemp > state.targetTemp + 25) {212 gauge.classList.remove('text-primary-500', 'text-green-500');213 gauge.classList.add('text-red-500');214 } else if (Math.abs(state.currentTemp - state.targetTemp) < 10) {215 gauge.classList.remove('text-primary-500', 'text-red-500');216 gauge.classList.add('text-green-500');217 } else {218 gauge.classList.remove('text-green-500', 'text-red-500');219 gauge.classList.add('text-primary-500');220 }221 }222 223 // Update target display224 const targetDisplay = document.getElementById('target-temp-display');225 if (targetDisplay) {226 targetDisplay.textContent = `${Math.round(convertTemp(state.targetTemp))}°${state.unit}`;227 }228 229 // Update slider230 const slider = document.getElementById('target-slider');231 if (slider && slider.value != state.targetTemp) {232 slider.value = state.targetTemp;233 }234 235 // Update probes236 document.getElementById('probe1-temp') && (document.getElementById('probe1-temp').textContent = Math.round(convertTemp(state.probes[0].temp)));237 document.getElementById('probe2-temp') && (document.getElementById('probe2-temp').textContent = Math.round(convertTemp(state.probes[1].temp)));238 239 // Update progress bars240 const p1Progress = Math.min(100, (state.probes[0].temp / state.probes[0].target) * 100);241 const p2Progress = Math.min(100, (state.probes[1].temp / state.probes[1].target) * 100);242 document.getElementById('probe1-progress') && (document.getElementById('probe1-progress').style.width = `${p1Progress}%`);243 document.getElementById('probe2-progress') && (document.getElementById('probe2-progress').style.width = `${p2Progress}%`);244 245 // Update status indicators246 document.getElementById('fan-speed') && (document.getElementById('fan-speed').textContent = `${Math.round(state.fanSpeed)}%`);247 document.getElementById('cook-timer') && (document.getElementById('cook-timer').textContent = formatTime(state.cookTime));248 document.getElementById('battery-level') && (document.getElementById('battery-level').textContent = `${state.batteryLevel}%`);249 250 // Update probe statuses251 updateProbeStatus(0);252 updateProbeStatus(1);253}254 255function updateProbeStatus(index) {256 const probe = state.probes[index];257 const statusEl = document.getElementById(`probe${index + 1}-status`);258 if (!statusEl) return;259 260 const progress = probe.temp / probe.target;261 if (progress >= 1) {262 statusEl.textContent = 'Done';263 statusEl.className = 'px-2 py-0.5 text-xs rounded-full bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400';264 } else if (progress > 0.9) {265 statusEl.textContent = 'Almost';266 statusEl.className = 'px-2 py-0.5 text-xs rounded-full bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400';267 } else {268 statusEl.textContent = index === 0 ? 'Cooking' : 'Smoking';269 statusEl.className = `px-2 py-0.5 text-xs rounded-full ${index === 0 ? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400' : 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400'}`;270 }271}272 273function convertTemp(f) {274 return state.unit === 'C' ? (f - 32) * 5 / 9 : f;275}276 277function formatTime(seconds) {278 const hrs = Math.floor(seconds / 3600);279 const mins = Math.floor((seconds % 3600) / 60);280 const secs = seconds % 60;281 return `${hrs.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;282}283 284// Event listeners285function setupEventListeners() {286 // Target temperature slider287 const slider = document.getElementById('target-slider');288 if (slider) {289 slider.addEventListener('input', (e) => {290 state.targetTemp = parseInt(e.target.value);291 updateDisplay();292 updatePresetButtons();293 });294 }295 296 // Chart range selector297 const chartRange = document.getElementById('chart-range');298 if (chartRange) {299 chartRange.addEventListener('change', (e) => {300 showToast('Chart range updated', 'success');301 });302 }303 304 // Unit toggle305 document.getElementById('unit-f')?.addEventListener('click', () => setUnit('F'));306 document.getElementById('unit-c')?.addEventListener('click', () => setUnit('C'));307 308 // Theme toggle309 document.getElementById('theme-toggle')?.addEventListener('click', toggleTheme);310 311 // Check system preference312 if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {313 document.documentElement.classList.add('dark');314 }315}316 317function setPreset(temp) {318 state.targetTemp = temp;319 updateDisplay();320 updatePresetButtons();321 showToast(`Preset loaded: ${temp}°F`, 'success');322}323 324function updatePresetButtons() {325 const presets = [225, 275, 350];326 const buttons = document.querySelectorAll('.preset-btn');327 buttons.forEach((btn, i) => {328 if (presets[i] === state.targetTemp) {329 btn.classList.add('active');330 } else {331 btn.classList.remove('active');332 }333 });334}335 336function setUnit(unit) {337 state.unit = unit;338 document.getElementById('unit-f').className = `px-3 py-1.5 text-sm font-medium rounded-md ${unit === 'F' ? 'bg-white dark:bg-gray-600 shadow-sm text-gray-800 dark:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'}`;339 document.getElementById('unit-c').className = `px-3 py-1.5 text-sm font-medium rounded-md ${unit === 'C' ? 'bg-white dark:bg-gray-600 shadow-sm text-gray-800 dark:text-white' : 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'}`;340 updateDisplay();341 updateChart();342}343 344function toggleTheme() {345 document.documentElement.classList.toggle('dark');346 if (state.chart) {347 const isDark = document.documentElement.classList.contains('dark');348 state.chart.options.plugins.legend.labels.color = isDark ? '#9ca3af' : '#6b7280';349 state.chart.options.plugins.tooltip.backgroundColor = isDark ? '#1f2937' : '#ffffff';350 state.chart.options.plugins.tooltip.titleColor = isDark ? '#f3f4f6' : '#111827';351 state.chart.options.plugins.tooltip.bodyColor = isDark ? '#d1d5db' : '#4b5563';352 state.chart.options.plugins.tooltip.borderColor = isDark ? '#374151' : '#e5e7eb';353 state.chart.options.scales.x.ticks.color = isDark ? '#6b7280' : '#9ca3af';354 state.chart.options.scales.y.ticks.color = isDark ? '#6b7280' : '#9ca3af';355 state.chart.options.scales.y.grid.color = isDark ? '#374151' : '#f3f4f6';356 state.chart.update();357 }358}359 360function loadRecipe(recipe) {361 const recipes = {362 brisket: { target: 225, probe1: 195, probe2: null, time: '12-16 hrs' },363 ribs: { target: 225, probe1: 195, probe2: null, time: '5-6 hrs' },364 chicken: { target: 325, probe1: 165, probe2: null, time: '1.5-2 hrs' }365 };366 367 const r = recipes[recipe];368 state.targetTemp = r.target;369 state.probes[0].target = r.probe1;370 state.probes[0].temp = 80;371 state.cookTime = 0;372 373 updateDisplay();374 updatePresetButtons();375 showToast(`${recipe.charAt(0).toUpperCase() + recipe.slice(1)} recipe loaded!`, 'success');376 377 // Switch to dashboard378 navigateTo('dashboard');379}380 381function renderHistory() {382 const tbody = document.getElementById('history-table');383 if (!tbody) return;384 385 const history = [386 { date: '2024-01-15', meat: 'Brisket', duration: '14:32', peak: '234°F', result: 'Perfect', rating: 5 },387 { date: '2024-01-12', meat: 'Pork Ribs', duration: '5:45', peak: '228°F', result: 'Great', rating: 4 },388 { date: '2024-01-08', meat: 'Chicken', duration: '2:15', peak: '340°F', result: 'Good', rating: 4 },389 { date: '2024-01-05', meat: 'Brisket', duration: '16:20', peak: '225°F', result: 'Perfect', rating: 5 },390 { date: '2024-01-02', meat: 'Turkey', duration: '4:30', peak: '325°F', result: 'Excellent', rating: 5 },391 { date: '2023-12-28', meat: 'Pulled Pork', duration: '12:00', peak: '230°F', result: 'Good', rating: 4 },392 { date: '2023-12-25', meat: 'Prime Rib', duration: '3:45', peak: '350°F', result: 'Perfect', rating: 5 },393 { date: '2023-12-20', meat: 'Brisket', duration: '15:10', peak: '235°F', result: 'Great', rating: 4 }394 ];395 396 tbody.innerHTML = history.map(h => `397 <tr class="border-b border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition">398 <td class="py-4">399 <div class="font-medium text-gray-800 dark:text-white">${new Date(h.date).toLocaleDateString()}</div>400 <div class="text-xs text-gray-500">Evening cook</div>401 </td>402 <td class="py-4">403 <div class="flex items-center gap-2">404 <div class="w-8 h-8 rounded-lg bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">405 <span class="text-lg">🍖</span>406 </div>407 <span class="font-medium text-gray-800 dark:text-white">${h.meat}</span>408 </div>409 </td>410 <td class="py-4 text-gray-600 dark:text-gray-300">${h.duration}</td>411 <td class="py-4">412 <span class="px-2 py-1 text-xs font-medium bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-400 rounded-lg">413 ${h.peak}414 </span>415 </td>416 <td class="py-4">417 <span class="px-2 py-1 text-xs font-medium ${getResultClass(h.result)} rounded-lg">418 ${h.result}419 </span>420 </td>421 <td class="py-4">422 <button class="p-2 text-gray-400 hover:text-primary-500 transition">423 <i data-feather="more-horizontal" class="w-4 h-4"></i>424 </button>425 </td>426 </tr>427 `).join('');428 429 feather.replace();430}431 432function getResultClass(result) {433 const classes = {434 'Perfect': 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400',435 'Excellent': 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400',436 'Great': 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400',437 'Good': 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400'438 };439 return classes[result] || 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300';440}441 442// Navigation443function navigateTo(view) {444 // Hide all views445 document.querySelectorAll('.view').forEach(v => {446 v.classList.remove('active');447 setTimeout(() => {448 if (!v.classList.contains('active')) v.classList.add('hidden');449 }, 300);450 });451 452 // Show target view453 const target = document.getElementById(`${view}-view`);454 if (target) {455 target.classList.remove('hidden');456 setTimeout(() => target.classList.add('active'), 10);457 }458 459 state.currentView = view;460 461 // Update nav462 document.querySelectorAll('.nav-item').forEach(item => {463 item.classList.remove('active', 'bg-primary-50', 'dark:bg-primary-900/20', 'text-primary-600', 'dark:text-primary-400');464 if (item.dataset.view === view) {465 item.classList.add('active', 'bg-primary-50', 'dark:bg-primary-900/20', 'text-primary-600', 'dark:text-primary-400');466 }467 });468}469 470// Toast notifications471function showToast(message, type = 'info') {472 const container = document.querySelector('.toast-container') || createToastContainer();473 474 const toast = document.createElement('div');475 toast.className = `toast ${type}`;476 toast.innerHTML = `477 <i data-feather="${type === 'success' ? 'check-circle' : type === 'error' ? 'x-circle' : 'info'}" class="w-5 h-5 ${type === 'success' ? 'text-green-500' : type === 'error' ? 'text-red-500' : 'text-blue-500'}"></i>478 <span class="text-sm font-medium text-gray-800 dark:text-white">${message}</span>479 `;480 481 container.appendChild(toast);482 feather.replace();483 484 setTimeout(() => {485 toast.style.animation = 'slideIn 0.3s ease reverse';486 setTimeout(() => toast.remove(), 300);487 }, 4000);488}489 490function createToastContainer() {491 const container = document.createElement('div');492 container.className = 'toast-container';493 document.body.appendChild(container);494 return container;495}496 497// Expose functions globally498window.setPreset = setPreset;499window.loadRecipe = loadRecipe;500window.navigateTo = navigateTo;