CoolFace
Apppublic

smitchel/emberguard-bbq-master

sourceHugging Faceupdated 8mo agoView on Hugging Face
0likes
script.js500 linesDownload Raw Back to root
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;