Compare commits
7 Commits
be844bc4d0
...
main
| Author | SHA256 | Date | |
|---|---|---|---|
| 91bcb528c1 | |||
| d06b32c1b7 | |||
| 212dea6a7d | |||
| af89ca96a1 | |||
| 981a736821 | |||
| 3dd3f80b04 | |||
| 976fd9f647 |
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.
|
||||||
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;
|
||||||
|
}
|
||||||
79
src/roast.ts
79
src/roast.ts
@@ -1,4 +1,5 @@
|
|||||||
import { loadGraph } from './tempgraph.ts';
|
import { loadGraph } from './tempgraph.ts';
|
||||||
|
import { savePlan, retrievePlan, clearAllPlans, listSavedPlans } from './planStorage.ts';
|
||||||
|
|
||||||
let timer = 0;
|
let timer = 0;
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
@@ -275,53 +276,61 @@ function saveCurrentPlan(): void {
|
|||||||
const planName = nameInput.value.trim();
|
const planName = nameInput.value.trim();
|
||||||
const rawInput = (document.getElementById("plan-input") as HTMLInputElement).value.trim();
|
const rawInput = (document.getElementById("plan-input") as HTMLInputElement).value.trim();
|
||||||
|
|
||||||
if (!planName) {
|
try {
|
||||||
alert("Please enter a name to save the plan.");
|
savePlan(planName, rawInput);
|
||||||
return;
|
alert(`Plan "${planName}" saved successfully.`);
|
||||||
|
} catch (error) {
|
||||||
|
alert(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rawInput) {
|
|
||||||
alert("Temperature list is empty.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsed = rawInput.split(',').map(x => x.trim()).map(Number);
|
|
||||||
if (parsed.some(val => isNaN(val))) {
|
|
||||||
alert("Invalid temperature list. Please enter comma-separated numbers.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tempStr = parsed.join(",");
|
|
||||||
localStorage.setItem(`roastplan_${planName}`, tempStr);
|
|
||||||
nameInput.value = "";
|
nameInput.value = "";
|
||||||
updateSavedPlansDropdown();
|
updateSavedPlansDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadSavedPlan(name: string): void {
|
function loadSavedPlan(name: string): void {
|
||||||
if (!name) return;
|
const stored = retrievePlan(name);
|
||||||
const stored = localStorage.getItem(`roastplan_${name}`);
|
|
||||||
if (stored) {
|
if (stored) {
|
||||||
targets = stored.split(",").map(Number);
|
targets = stored.split(",").map(Number);
|
||||||
currentTargetIndex = 0;
|
currentTargetIndex = 0;
|
||||||
document.getElementById("controls")!.classList.remove("hidden");
|
document.getElementById("controls")!.classList.remove("hidden");
|
||||||
document.getElementById("create-plan")!.classList.add("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();
|
generateTempButtons();
|
||||||
|
|
||||||
|
// Reset the input dropdown to the first (choose prompt) entry
|
||||||
|
updateSavedPlansDropdown();
|
||||||
|
} else {
|
||||||
|
alert(`No plan found with the name "${name}".`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSavedPlansDropdown(): void {
|
function updateSavedPlansDropdown(): void {
|
||||||
const dropdown = document.getElementById("saved-plans") as HTMLSelectElement;
|
const dropdown = document.getElementById("saved-plans") as HTMLSelectElement;
|
||||||
dropdown.innerHTML = '<option value="">-- Select saved plan --</option>';
|
|
||||||
for (let i = 0; i < localStorage.length; i++) {
|
// Retrieve saved plans from planStorage.ts
|
||||||
const key = localStorage.key(i);
|
const savedPlans = listSavedPlans();
|
||||||
if (key && key.startsWith("roastplan_")) {
|
|
||||||
const name = key.replace("roastplan_", "");
|
// Reset to default prompt
|
||||||
const option = document.createElement("option");
|
dropdown.innerHTML = '<option value="">-- Select Saved Plan --</option>';
|
||||||
option.value = name;
|
|
||||||
option.textContent = name;
|
// Populate the dropdown with saved plans
|
||||||
dropdown.appendChild(option);
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,16 +338,7 @@ function clearSavedPlans(): void {
|
|||||||
const confirmClear = confirm("Are you sure you want to delete all saved roast plans?");
|
const confirmClear = confirm("Are you sure you want to delete all saved roast plans?");
|
||||||
if (!confirmClear) return;
|
if (!confirmClear) return;
|
||||||
|
|
||||||
const keysToRemove: string[] = [];
|
clearAllPlans();
|
||||||
|
|
||||||
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));
|
|
||||||
updateSavedPlansDropdown();
|
updateSavedPlansDropdown();
|
||||||
alert("All saved roast plans have been deleted.");
|
alert("All saved roast plans have been deleted.");
|
||||||
}
|
}
|
||||||
@@ -364,5 +364,4 @@ if (typeof window !== 'undefined') {
|
|||||||
window.clearSavedPlans = clearSavedPlans;
|
window.clearSavedPlans = clearSavedPlans;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export {};
|
export {};
|
||||||
|
|||||||
Reference in New Issue
Block a user