Compare commits
11 Commits
b0024aec80
...
main
| Author | SHA256 | Date | |
|---|---|---|---|
| 91bcb528c1 | |||
| d06b32c1b7 | |||
| 212dea6a7d | |||
| af89ca96a1 | |||
| 981a736821 | |||
| 3dd3f80b04 | |||
| 976fd9f647 | |||
| be844bc4d0 | |||
| a90824efcd | |||
| af68e8923c | |||
| 15f74335c7 |
25
LICENSE
Normal file
25
LICENSE
Normal file
@@ -0,0 +1,25 @@
|
||||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2025, Terrence Ezrol (ezterry)
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
66
README.MD
Normal file
66
README.MD
Normal file
@@ -0,0 +1,66 @@
|
||||
# Manual Coffee Roasting Plan
|
||||
|
||||
**Project Name:** Manual Coffee Roasting Plan
|
||||
**Hosting:** [View on Gitea](https://git-hojo.devnull.name/ezterry/Manual-Coffee-Roasting-Plan)
|
||||
|
||||
---
|
||||
|
||||
### Summary
|
||||
A tool to track and visualize the manual coffee roasting process, including timer functionality and potential chart-based roasting curve tracking.
|
||||
|
||||
---
|
||||
|
||||
### Installation
|
||||
1. Clone the repository.
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
# or
|
||||
yarn install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Development Environment (VS Code)
|
||||
1. Open the project in VS Code.
|
||||
2. Ensure the following extensions are installed:
|
||||
- **TypeScript** (for `.ts` file support)
|
||||
- **Parcel** (for bundling)
|
||||
3. Start the dev server:
|
||||
```bash
|
||||
npm start
|
||||
# or
|
||||
yarn start
|
||||
```
|
||||
This will serve the app at `http://localhost:1234` (default Parcel port).
|
||||
|
||||
---
|
||||
|
||||
### Deployment
|
||||
Build the production-ready files:
|
||||
```bash
|
||||
npm run build
|
||||
# or
|
||||
yarn build
|
||||
```
|
||||
The output will be in the `dist/` folder. Deploy this folder to any static hosting service (e.g., GitHub Pages, Netlify, or Gitea's built-in static hosting).
|
||||
|
||||
---
|
||||
|
||||
### Technologies Used
|
||||
- **TypeScript** (`src/roast.ts`) for type-safe logic.
|
||||
- **Chart.js** and **chartjs-plugin-annotation** for potential roasting curve visualizations.
|
||||
- **Parcel** for bundling and optimization.
|
||||
- **PostCSS** and **Autoprefixer** for CSS compatibility.
|
||||
|
||||
---
|
||||
|
||||
### Notes
|
||||
- The core logic is in toast.ts
|
||||
- The graph visual is in tempgraph.ts
|
||||
- roast.html is the main web app, this is all static server content but the typescript may save some local information at your request on your system
|
||||
|
||||
---
|
||||
|
||||
### Contributing
|
||||
Pull requests are welcome! For major changes, please open an issue first.
|
||||
40
TODO.md
Normal file
40
TODO.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# TODO List Template for Project
|
||||
|
||||
## Overview
|
||||
This template outlines the current state of the project files and future tasks to improve organization and functionality.
|
||||
|
||||
### Current State
|
||||
|
||||
#### `roast.ts`
|
||||
- Manages timer functions (`startTimer`, `stopTimer`).
|
||||
- Handles event logging (`logEvent`) and system reset.
|
||||
- Provides plan setting and navigation through temperature buttons.
|
||||
- Implements CSV export functionality.
|
||||
- Supports graph display and manipulation.
|
||||
- Manages saved roast plans (save, load, clear).
|
||||
|
||||
#### `roast.html`
|
||||
- Defines the user interface for coffee roast logging.
|
||||
- Includes input fields for setting roast plans and saving them.
|
||||
- Contains control elements to manage events and export data.
|
||||
|
||||
#### `roast.css`
|
||||
- Styles the timer display with phase-specific backgrounds.
|
||||
- Formats buttons and containers for a cohesive visual layout.
|
||||
- Ensures responsiveness and accessibility across devices.
|
||||
|
||||
### Future Tasks
|
||||
|
||||
1. **Enhance User Interface:**
|
||||
- Update visual styling for better user experience in `roast.css`.
|
||||
- Implement a popup library window for managing roast plans, allowing users to save, restore, edit, and delete plans.
|
||||
- Integrate access to hard-coded library plans that can be added to the user's saved list if the online library changes.
|
||||
|
||||
2. **Expand Functionality:**
|
||||
- Add information about the rate of rise during roasting.
|
||||
- Capture details about the roast (start/end weight).
|
||||
- Implement a favorite button for quick access to frequently used plans.
|
||||
|
||||
3. **Documentation and Testing:**
|
||||
- Create comprehensive documentation for all features.
|
||||
- Develop test cases to ensure robustness of functionality.
|
||||
16307
package-lock.json
generated
16307
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,14 +4,15 @@
|
||||
"chartjs-plugin-annotation": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.15.31",
|
||||
"parcel": "^2.15.2",
|
||||
"@types/node": "^22.16.3",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"parcel": "^2.15.4",
|
||||
"postcss": "^8.5.6",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "parcel src/roast.html",
|
||||
"build": "parcel build src/roast.html --public-url=./ --dist-dir dist"
|
||||
|
||||
},
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
|
||||
74
src/planStorage.ts
Normal file
74
src/planStorage.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// planStorage.ts
|
||||
|
||||
/**
|
||||
* Saves a roast plan to local storage.
|
||||
*
|
||||
* @param name The name of the plan.
|
||||
* @param temperatures A comma-separated string of temperature targets.
|
||||
*/
|
||||
export function savePlan(name: string, temperatures: string): void {
|
||||
if (!name) {
|
||||
throw new Error("Please provide a valid name for the roast plan.");
|
||||
}
|
||||
|
||||
const trimmedTemperatures = temperatures.trim();
|
||||
if (!trimmedTemperatures) {
|
||||
throw new Error("Temperature list is empty.");
|
||||
}
|
||||
|
||||
// Parse and validate temperature input
|
||||
const parsedTemps = trimmedTemperatures.split(',').map(x => x.trim()).map(Number);
|
||||
if (parsedTemps.some(val => isNaN(val))) {
|
||||
throw new Error("Invalid temperature list. Please enter comma-separated numbers.");
|
||||
}
|
||||
|
||||
localStorage.setItem(`roastplan_${name}`, trimmedTemperatures);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a saved roast plan from local storage.
|
||||
*
|
||||
* @param name The name of the plan to load.
|
||||
* @returns A string containing the temperature targets, or null if not found.
|
||||
*/
|
||||
export function retrievePlan(name: string): string | null {
|
||||
if (!name) return null;
|
||||
|
||||
const stored = localStorage.getItem(`roastplan_${name}`);
|
||||
return stored || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all saved roast plans from local storage.
|
||||
*/
|
||||
export function clearAllPlans(): void {
|
||||
const keysToRemove: string[] = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith("roastplan_")) {
|
||||
keysToRemove.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all saved roast plan names from local storage.
|
||||
*
|
||||
* @returns An array of strings representing the names of saved plans.
|
||||
*/
|
||||
export function listSavedPlans(): string[] {
|
||||
const savedPlanNames: string[] = [];
|
||||
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith("roastplan_")) {
|
||||
const planName = key.replace("roastplan_", "");
|
||||
savedPlanNames.push(planName);
|
||||
}
|
||||
}
|
||||
|
||||
return savedPlanNames;
|
||||
}
|
||||
@@ -96,6 +96,13 @@ body {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
#event-buttons button.activated {
|
||||
background-color: #ccc; /* gray to indicate completed step */
|
||||
color: #333;
|
||||
border: 2px solid #888;
|
||||
}
|
||||
|
||||
|
||||
#event-buttons button {
|
||||
padding: 15px 25px;
|
||||
font-size: 20px;
|
||||
@@ -110,6 +117,14 @@ body {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.used-temp {
|
||||
background-color: #ccc !important;
|
||||
color: #333;
|
||||
border: 2px solid #888;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
||||
#log-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
@@ -13,7 +13,23 @@
|
||||
<div id="create-plan">
|
||||
<input type="text" id="plan-input" placeholder="Enter temperatures (comma separated)">
|
||||
<button onclick="setPlan()">Set Plan</button>
|
||||
|
||||
<!-- Save and Load Plan UI -->
|
||||
<div id="plan-storage" style="margin-top: 16px;">
|
||||
<input type="text" id="save-name" placeholder="Save plan as..." style="width: 60%; padding: 8px;">
|
||||
<button onclick="saveCurrentPlan()" style="padding: 8px 16px;">Save Plan</button>
|
||||
|
||||
<br><br>
|
||||
<button onclick="clearSavedPlans()" style="margin-top: 10px; padding: 6px 16px;">Clear All Plans</button>
|
||||
|
||||
<label for="saved-plans">Load a saved plan:</label>
|
||||
<select id="saved-plans" onchange="loadSavedPlan(this.value)" style="margin-left: 8px; padding: 6px;">
|
||||
<option value="">-- Select saved plan --</option>
|
||||
</select>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="controls" class="hidden">
|
||||
<div id="temp-buttons" class="button-group"></div>
|
||||
<div id="event-buttons" class="button-group">
|
||||
|
||||
165
src/roast.ts
165
src/roast.ts
@@ -1,4 +1,5 @@
|
||||
import { loadGraph } from './tempgraph.ts';
|
||||
import { savePlan, retrievePlan, clearAllPlans, listSavedPlans } from './planStorage.ts';
|
||||
|
||||
let timer = 0;
|
||||
let isRunning = false;
|
||||
@@ -7,6 +8,7 @@ let targets: number[] = [];
|
||||
let currentTargetIndex = 0;
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
let firstCrackTime: number | null = null;
|
||||
let loggedTemps = new Set<number>();
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -20,6 +22,9 @@ declare global {
|
||||
exportCSV: () => void;
|
||||
showGraph: () => void;
|
||||
hideGraph: () => void;
|
||||
saveCurrentPlan: () => void;
|
||||
loadSavedPlan: (name: string) => void;
|
||||
clearSavedPlans: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +44,7 @@ function startTimer(): void {
|
||||
}
|
||||
|
||||
function stopTimer(): void {
|
||||
if(interval !== null){
|
||||
if (interval !== null) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
isRunning = false;
|
||||
@@ -48,7 +53,7 @@ function stopTimer(): void {
|
||||
function updateTimerDisplay(): void {
|
||||
const roastTimeEl = document.getElementById("roastTime")!;
|
||||
const devPercentEl = document.getElementById("devPercent")!;
|
||||
|
||||
|
||||
const minutes = Math.floor(timer / 60);
|
||||
const seconds = timer % 60;
|
||||
roastTimeEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
||||
@@ -63,11 +68,8 @@ function updateTimerDisplay(): void {
|
||||
}
|
||||
|
||||
const timerEl = document.getElementById("timer")!;
|
||||
|
||||
// Reset classes
|
||||
timerEl.className = "";
|
||||
|
||||
// Determine phase
|
||||
if (!log.some(e => e.event === "Charge")) {
|
||||
timerEl.classList.add("initial-phase");
|
||||
} else if (!log.some(e => e.event === "Yellow")) {
|
||||
@@ -75,26 +77,20 @@ function updateTimerDisplay(): void {
|
||||
} else if (firstCrackTime === null) {
|
||||
timerEl.classList.add("yellow-phase");
|
||||
} else {
|
||||
// Development phase
|
||||
timerEl.classList.add("dev-phase");
|
||||
|
||||
const devTime = timer - firstCrackTime;
|
||||
const devRatio = Math.min(devTime / timer, 0.4); // clamp to 0–0.4
|
||||
|
||||
// Interpolate from yellow (255,255,208) to muddy brown (215,195,158)
|
||||
const devRatio = Math.min(devTime / timer, 0.4);
|
||||
const r = Math.round(255 - devRatio * 100);
|
||||
const g = Math.round(255 - devRatio * 150);
|
||||
const b = Math.round(208 - devRatio * 125);
|
||||
|
||||
timerEl.style.backgroundColor = `rgb(${r},${g},${b})`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function logEvent(eventName: string): void {
|
||||
if (eventName === "Charge") {
|
||||
if (log.length > 0 || isRunning) {
|
||||
if(isRunning){
|
||||
if (isRunning) {
|
||||
stopTimer();
|
||||
}
|
||||
log = [];
|
||||
@@ -103,6 +99,7 @@ function logEvent(eventName: string): void {
|
||||
startTimer();
|
||||
log.push({ time: formatTime(timer), event: eventName });
|
||||
updateLog();
|
||||
updateEventButtons(eventName);
|
||||
return;
|
||||
}
|
||||
if (eventName === "Drop") {
|
||||
@@ -110,8 +107,8 @@ function logEvent(eventName: string): void {
|
||||
log.push({ time: formatTime(timer), event: eventName });
|
||||
updateLog();
|
||||
document.getElementById("create-plan")!.classList.remove("hidden");
|
||||
const tempButtons = document.getElementById("temp-buttons")!;
|
||||
tempButtons.innerHTML = "";
|
||||
document.getElementById("temp-buttons")!.innerHTML = "";
|
||||
updateEventButtons(eventName);
|
||||
return;
|
||||
}
|
||||
if (eventName === "First Crack") {
|
||||
@@ -120,6 +117,22 @@ function logEvent(eventName: string): void {
|
||||
|
||||
log.push({ time: formatTime(timer), event: eventName });
|
||||
updateLog();
|
||||
updateEventButtons(eventName);
|
||||
}
|
||||
|
||||
function updateEventButtons(eventName: string): void {
|
||||
const eventSequence = ["Charge", "Yellow", "First Crack", "Second Crack", "Drop"];
|
||||
if (eventSequence.includes(eventName)) {
|
||||
const pressedIndex = eventSequence.indexOf(eventName);
|
||||
for (let i = 0; i <= pressedIndex; i++) {
|
||||
const btn = document.querySelector(`#event-buttons button:nth-child(${i + 1})`) as HTMLButtonElement;
|
||||
if (btn) btn.classList.add("activated");
|
||||
}
|
||||
} else if (eventName === "Reset") {
|
||||
document.querySelectorAll("#event-buttons button").forEach(btn =>
|
||||
btn.classList.remove("activated")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function resetSystem(): void {
|
||||
@@ -127,6 +140,7 @@ function resetSystem(): void {
|
||||
log = [];
|
||||
timer = 0;
|
||||
firstCrackTime = null;
|
||||
loggedTemps.clear();
|
||||
updateLog();
|
||||
updateTimerDisplay();
|
||||
|
||||
@@ -134,12 +148,13 @@ function resetSystem(): void {
|
||||
devPercentEl.style.display = "none";
|
||||
|
||||
const timerEl = document.getElementById("timer")!;
|
||||
timerEl.className = "initial-phase"; // back to white background
|
||||
timerEl.style.backgroundColor = ""; // clear inline style
|
||||
timerEl.className = "initial-phase";
|
||||
timerEl.style.backgroundColor = "";
|
||||
|
||||
document.getElementById("create-plan")!.classList.remove("hidden");
|
||||
const tempButtons = document.getElementById("temp-buttons")!;
|
||||
tempButtons.innerHTML = "";
|
||||
document.getElementById("temp-buttons")!.innerHTML = "";
|
||||
|
||||
updateEventButtons("Reset");
|
||||
}
|
||||
|
||||
function formatTime(seconds: number): string {
|
||||
@@ -160,17 +175,24 @@ function setPlan(): void {
|
||||
function generateTempButtons(): void {
|
||||
const container = document.getElementById("temp-buttons")!;
|
||||
container.innerHTML = "";
|
||||
|
||||
if (targets.length === 0) {
|
||||
container.innerHTML = "<p>No temperature plan set.</p>";
|
||||
return;
|
||||
}
|
||||
|
||||
const currentTarget = targets[currentTargetIndex];
|
||||
const buttons: string[] = [];
|
||||
|
||||
buttons.push('<button class="nav-btn" onclick="navigateTemp(-1);"><<</button>');
|
||||
|
||||
for (let i = -2; i <= 2; i++) {
|
||||
const temp = currentTarget + i;
|
||||
buttons.push(`<button onclick="logTemp(${temp})">${temp}</button>`);
|
||||
const used = loggedTemps.has(temp);
|
||||
const classStr = used ? 'used-temp' : '';
|
||||
buttons.push(`<button onclick="logTemp(${temp})" class="${classStr}">${temp}</button>`);
|
||||
}
|
||||
|
||||
buttons.push('<button class="nav-btn" onclick="navigateTemp(1);">>></button>');
|
||||
container.innerHTML = buttons.join(" ");
|
||||
}
|
||||
@@ -187,6 +209,7 @@ function navigateTemp(direction: number): void {
|
||||
|
||||
function logTemp(temp: number): void {
|
||||
log.push({ time: formatTime(timer), event: "Temp", temp });
|
||||
loggedTemps.add(temp);
|
||||
updateLog();
|
||||
currentTargetIndex++;
|
||||
if (currentTargetIndex >= targets.length) {
|
||||
@@ -206,22 +229,16 @@ function updateLog(): void {
|
||||
`).join("");
|
||||
}
|
||||
|
||||
// src/roast.ts
|
||||
|
||||
/**
|
||||
* Generates an array of CSV strings line by line from log data.
|
||||
*/
|
||||
function createCSVData(): string[] {
|
||||
const header = "Time,Event,Temperature";
|
||||
const logEntries = log.map(entry => `${entry.time},${entry.event}${entry.temp !== null ? `,${entry.temp}` : ''}`);
|
||||
return [header].concat(logEntries);
|
||||
const header = "Time,Event,Temperature";
|
||||
const logEntries = log.map(entry =>
|
||||
`${entry.time},${entry.event}${entry.temp !== null ? `,${entry.temp}` : ''}`
|
||||
);
|
||||
return [header, ...logEntries];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates CSV format content as a single string from the array returned by createCSVData().
|
||||
*/
|
||||
function generateCSVContent(): string {
|
||||
return createCSVData().join("\n");
|
||||
return createCSVData().join("\n");
|
||||
}
|
||||
|
||||
function exportCSV(): void {
|
||||
@@ -251,9 +268,86 @@ function hideGraph(): void {
|
||||
graphContainer.classList.add("hidden");
|
||||
graphContainer.classList.remove("graph-display-flex");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expose functions to the global scope (allowing html direct access)
|
||||
function saveCurrentPlan(): void {
|
||||
const nameInput = document.getElementById("save-name") as HTMLInputElement;
|
||||
const planName = nameInput.value.trim();
|
||||
const rawInput = (document.getElementById("plan-input") as HTMLInputElement).value.trim();
|
||||
|
||||
try {
|
||||
savePlan(planName, rawInput);
|
||||
alert(`Plan "${planName}" saved successfully.`);
|
||||
} catch (error) {
|
||||
alert(error.message);
|
||||
}
|
||||
|
||||
nameInput.value = "";
|
||||
updateSavedPlansDropdown();
|
||||
}
|
||||
|
||||
function loadSavedPlan(name: string): void {
|
||||
const stored = retrievePlan(name);
|
||||
if (stored) {
|
||||
targets = stored.split(",").map(Number);
|
||||
currentTargetIndex = 0;
|
||||
document.getElementById("controls")!.classList.remove("hidden");
|
||||
document.getElementById("create-plan")!.classList.add("hidden");
|
||||
|
||||
// Set the plan in the text box
|
||||
console.log("Set plan: " +stored);
|
||||
const planInput = document.getElementById("plan-input") as HTMLInputElement;
|
||||
const savedName = document.getElementById("save-name") as HTMLInputElement;
|
||||
planInput.value = stored;
|
||||
savedName.value = name;
|
||||
|
||||
generateTempButtons();
|
||||
|
||||
// Reset the input dropdown to the first (choose prompt) entry
|
||||
updateSavedPlansDropdown();
|
||||
} else {
|
||||
alert(`No plan found with the name "${name}".`);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSavedPlansDropdown(): void {
|
||||
const dropdown = document.getElementById("saved-plans") as HTMLSelectElement;
|
||||
|
||||
// Retrieve saved plans from planStorage.ts
|
||||
const savedPlans = listSavedPlans();
|
||||
|
||||
// Reset to default prompt
|
||||
dropdown.innerHTML = '<option value="">-- Select Saved Plan --</option>';
|
||||
|
||||
// Populate the dropdown with saved plans
|
||||
savedPlans.forEach(planName => {
|
||||
const option = document.createElement('option');
|
||||
option.value = planName;
|
||||
option.textContent = planName;
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
|
||||
// Set the first entry as selected if available
|
||||
if (savedPlans.length > 0) {
|
||||
dropdown.selectedIndex = 0; // Index 1 corresponds to the first actual plan
|
||||
}
|
||||
}
|
||||
|
||||
function clearSavedPlans(): void {
|
||||
const confirmClear = confirm("Are you sure you want to delete all saved roast plans?");
|
||||
if (!confirmClear) return;
|
||||
|
||||
clearAllPlans();
|
||||
updateSavedPlansDropdown();
|
||||
alert("All saved roast plans have been deleted.");
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
updateSavedPlansDropdown();
|
||||
});
|
||||
|
||||
// Include in window export
|
||||
if (typeof window !== 'undefined') {
|
||||
window.startTimer = startTimer;
|
||||
window.stopTimer = stopTimer;
|
||||
@@ -265,6 +359,9 @@ if (typeof window !== 'undefined') {
|
||||
window.exportCSV = exportCSV;
|
||||
window.showGraph = showGraph;
|
||||
window.hideGraph = hideGraph;
|
||||
window.saveCurrentPlan = saveCurrentPlan;
|
||||
window.loadSavedPlan = loadSavedPlan;
|
||||
window.clearSavedPlans = clearSavedPlans;
|
||||
}
|
||||
|
||||
export {};
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user