Compare commits
24 Commits
19416c7592
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 976e7d9136 | |||
| 6a22bac56c | |||
| 650b469202 | |||
| c4a15a95b3 | |||
| 90d9055667 | |||
| 238e7cdc97 | |||
| 85cf1b22cc | |||
| 3ef5908b09 | |||
| 271c8baafd | |||
| 7b66f395bb | |||
| 26d2b6b1db | |||
| b552b5e88e | |||
| f781bbae74 | |||
| 9a9c0b9ad0 | |||
| d3872aef40 | |||
| c7d4d8eb9e | |||
| 0a7020cb88 | |||
| e789f481f3 | |||
| 61b238fdf0 | |||
| 4d493b7d71 | |||
| f3baa12bd3 | |||
| 119f1905ec | |||
| 081c4c2a7f | |||
| f0666c247b |
@@ -1,22 +1,44 @@
|
|||||||
# 🐕 Sundog Dog Food Calorie Calculator
|
# 🐕 Sundog Dog Food Calorie Calculator
|
||||||
|
|
||||||
A professional veterinary nutrition tool for calculating dogs' daily calorie requirements and food amounts. Built for embedding on websites with complete brand protection options.
|
A professional veterinary nutrition tool for calculating dogs' daily calorie requirements and food amounts. Features advanced multi-food source management, percentage locking, and detailed food amount breakdowns. Built for embedding on websites with complete brand protection options.
|
||||||
|
|
||||||
**By [Canine Nutrition and Wellness](https://caninenutritionandwellness.com)**
|
**By [Canine Nutrition and Wellness](https://caninenutritionandwellness.com)**
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
|
### Core Calculation Features
|
||||||
- **Accurate Calculations**: Uses veterinary-standard RER formula: `70 × (weight in kg)^0.75`
|
- **Accurate Calculations**: Uses veterinary-standard RER formula: `70 × (weight in kg)^0.75`
|
||||||
- **Multiple Activity Levels**: 11 different dog types and activity factors
|
- **Multiple Activity Levels**: 11 different dog types and activity factors
|
||||||
- **Multi-Region Support**: kcal/100g (EU/UK), kcal/kg, kcal/cup, kcal/can (US/Canada)
|
- **Multi-Region Support**: kcal/100g (EU/UK), kcal/kg, kcal/cup, kcal/can (US/Canada)
|
||||||
- **Food Amount Calculator**: Converts calories to grams/kg/oz/lb of food
|
- **Unit Conversion**: Automatic conversion to grams/kg/oz/lb for food amounts
|
||||||
|
- **Smart Unit Selection**: Auto-selects grams for metric and ounces for imperial systems
|
||||||
|
- **Unit Selection Buttons**: Intuitive button interface for choosing display units (g/kg/oz/lb)
|
||||||
|
|
||||||
|
### Multi-Food Source Management
|
||||||
|
- **Multiple Food Sources**: Add up to 5 different food sources per diet plan
|
||||||
|
- **Percentage System**: Distribute diet percentages across multiple foods with real-time validation
|
||||||
|
- **Percentage Locking**: Lock specific food source percentages to maintain fixed ratios
|
||||||
|
- **Smart Redistribution**: Automatic percentage rebalancing when sources are added/removed
|
||||||
|
- **Editable Food Names**: Click-to-edit food source names (e.g., "Morning Kibble", "Evening Wet Food")
|
||||||
|
|
||||||
|
### Food Amount Breakdown
|
||||||
|
- **Individual Food Amounts**: See exact amounts needed for each food source
|
||||||
|
- **Per-Food Calculations**: Calculate specific quantities for different food types
|
||||||
|
- **Total Summary**: Combined totals with clear breakdown by food source
|
||||||
|
- **Lock Indicators**: Visual indicators showing which percentages are locked
|
||||||
|
|
||||||
|
### User Experience
|
||||||
- **Scalable Widget**: Easily resize from 50% to 200% with data attributes
|
- **Scalable Widget**: Easily resize from 50% to 200% with data attributes
|
||||||
- **Theme Support**: Light, dark, and system themes
|
- **Theme Support**: Light, dark, and system themes
|
||||||
- **Responsive Design**: Mobile-first, works on all devices
|
- **Responsive Design**: Mobile-first, optimized layouts for all devices
|
||||||
- **Brand Integration**: Uses Canine Nutrition and Wellness color scheme
|
|
||||||
- **Two Embedding Options**: JavaScript widget and iframe
|
- **Two Embedding Options**: JavaScript widget and iframe
|
||||||
- **Accessibility**: Full keyboard navigation and screen reader support
|
- **Accessibility**: Full keyboard navigation and screen reader support
|
||||||
|
|
||||||
|
### Brand & Integration
|
||||||
|
- **Brand Integration**: Uses Canine Nutrition and Wellness color scheme
|
||||||
|
- **Professional Design**: Clean, veterinary-grade interface
|
||||||
|
- **Brand Protection**: Complete iframe isolation option
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
### Option 1: JavaScript Widget (Recommended)
|
### Option 1: JavaScript Widget (Recommended)
|
||||||
@@ -50,6 +72,59 @@ A professional veterinary nutrition tool for calculating dogs' daily calorie req
|
|||||||
</iframe>
|
</iframe>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🍽️ Multi-Food Source Usage
|
||||||
|
|
||||||
|
The calculator supports complex feeding plans with multiple food sources, perfect for mixed diets combining dry food, wet food, treats, and supplements.
|
||||||
|
|
||||||
|
### Basic Multi-Food Workflow
|
||||||
|
|
||||||
|
1. **Start with one food source** - Calculator begins with a single "Food Source 1"
|
||||||
|
2. **Add more sources** - Click "Add another food source" (up to 5 total)
|
||||||
|
3. **Customize names** - Click any food source name to edit (e.g., "Morning Kibble", "Evening Wet Food")
|
||||||
|
4. **Set energy content** - Enter kcal values and select appropriate units for each food
|
||||||
|
5. **Adjust percentages** - Use sliders or input fields to distribute diet percentages
|
||||||
|
6. **Lock percentages** - Click 🔒 to lock specific food source percentages
|
||||||
|
7. **Get individual amounts** - See exact quantities needed for each food source
|
||||||
|
|
||||||
|
### Percentage System Features
|
||||||
|
|
||||||
|
- **Real-time Validation**: Percentages always total exactly 100%
|
||||||
|
- **Smart Redistribution**: When you change one percentage, others adjust automatically
|
||||||
|
- **Percentage Locking**: Lock specific sources to maintain fixed ratios
|
||||||
|
- **Visual Feedback**: Lock indicators show which percentages are fixed
|
||||||
|
- **Bulletproof Logic**: Prevents impossible states (negative percentages, >100% totals)
|
||||||
|
|
||||||
|
### Example Usage Scenarios
|
||||||
|
|
||||||
|
**Mixed Diet Example:**
|
||||||
|
```
|
||||||
|
Royal Canin Dry Food → 70% (locked)
|
||||||
|
Blue Buffalo Wet Food → 25%
|
||||||
|
Training Treats → 5%
|
||||||
|
```
|
||||||
|
|
||||||
|
**Meal-Based Planning:**
|
||||||
|
```
|
||||||
|
Morning Kibble → 50%
|
||||||
|
Evening Wet Food → 30%
|
||||||
|
Midday Snacks → 20%
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transition Diet:**
|
||||||
|
```
|
||||||
|
Old Food (reducing) → 25%
|
||||||
|
New Food (increasing) → 75% (locked)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Food Amount Breakdown
|
||||||
|
|
||||||
|
The calculator provides detailed breakdowns showing:
|
||||||
|
- **Individual amounts** for each food source
|
||||||
|
- **Percentage distribution** with visual indicators
|
||||||
|
- **Lock status** for each food source
|
||||||
|
- **Total combined amount** for the specified number of days
|
||||||
|
- **Unit conversion** (grams, kg, oz, lb) for all amounts
|
||||||
|
|
||||||
## ⚙️ Configuration Options
|
## ⚙️ Configuration Options
|
||||||
|
|
||||||
### Theme Options
|
### Theme Options
|
||||||
@@ -218,18 +293,39 @@ Your branding is completely protected in iframe mode. Users cannot:
|
|||||||
## 🧪 Testing
|
## 🧪 Testing
|
||||||
|
|
||||||
### Manual Testing Checklist
|
### Manual Testing Checklist
|
||||||
|
|
||||||
|
#### Core Functionality
|
||||||
- [ ] All dog type selections work
|
- [ ] All dog type selections work
|
||||||
- [ ] Weight validation (minimum 0.1kg)
|
- [ ] Weight validation (minimum 0.1kg)
|
||||||
- [ ] RER/MER calculations accurate
|
- [ ] RER/MER calculations accurate
|
||||||
- [ ] Food energy unit selector (kcal/100g, kcal/kg, kcal/cup, kcal/can)
|
|
||||||
- [ ] Food energy validation (minimum values per unit)
|
|
||||||
- [ ] Unit conversions (g/kg/oz/lb) correct
|
- [ ] Unit conversions (g/kg/oz/lb) correct
|
||||||
- [ ] Theme switching (light/dark/system)
|
- [ ] Theme switching (light/dark/system)
|
||||||
- [ ] Scale options (0.5x to 2.0x) work properly
|
- [ ] Scale options (0.5x to 2.0x) work properly
|
||||||
- [ ] Collapsible section toggles
|
|
||||||
|
#### Multi-Food Source Features
|
||||||
|
- [ ] Add food sources (up to 5 maximum)
|
||||||
|
- [ ] Remove food sources (minimum 1 maintained)
|
||||||
|
- [ ] Edit food source names (click-to-edit functionality)
|
||||||
|
- [ ] Food energy content validation per source
|
||||||
|
- [ ] Food energy unit selector per source (kcal/100g, kcal/kg, kcal/cup, kcal/can)
|
||||||
|
- [ ] Percentage slider adjustments work correctly
|
||||||
|
- [ ] Percentage input field validation
|
||||||
|
- [ ] Percentage locking/unlocking (🔒 icon)
|
||||||
|
- [ ] Smart percentage redistribution when sources change
|
||||||
|
- [ ] Total percentages always equal 100%
|
||||||
|
- [ ] Individual food amount calculations
|
||||||
|
- [ ] Food amount breakdown display
|
||||||
|
- [ ] Add button states ("Add another food source" vs "Maximum 5 sources reached")
|
||||||
|
|
||||||
|
#### User Interface
|
||||||
- [ ] Mobile responsive layout
|
- [ ] Mobile responsive layout
|
||||||
|
- [ ] Collapsible section toggles
|
||||||
|
- [ ] Visual lock indicators display correctly
|
||||||
|
- [ ] Percentage badges and styling
|
||||||
- [ ] Branded footer link works
|
- [ ] Branded footer link works
|
||||||
- [ ] Box shadows consistent across all sections
|
- [ ] Box shadows consistent across all sections
|
||||||
|
- [ ] Food source name alignment on mobile
|
||||||
|
- [ ] Proper input field sizing on mobile
|
||||||
|
|
||||||
### Browser Compatibility
|
### Browser Compatibility
|
||||||
- ✅ Chrome 90+
|
- ✅ Chrome 90+
|
||||||
@@ -257,8 +353,8 @@ This calculator is provided for educational and professional use. The formulas a
|
|||||||
## 🔗 Links
|
## 🔗 Links
|
||||||
|
|
||||||
- **Website**: [caninenutritionandwellness.com](https://caninenutritionandwellness.com)
|
- **Website**: [caninenutritionandwellness.com](https://caninenutritionandwellness.com)
|
||||||
- **Demo**: Open `embed-demo.html` in your browser
|
- **Widget Demo**: Open `test-widget.html` in your browser
|
||||||
- **Standalone**: Open `index.html` in your browser
|
- **Standalone**: Open `iframe.html` in your browser
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -100,17 +100,88 @@ function transformHTMLForWidget(html) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create production-ready widget JavaScript
|
* Create production-ready widget JavaScript that uses the ACTUAL extracted JS from iframe.html
|
||||||
*/
|
*/
|
||||||
function createWidgetJS(css, html, js) {
|
function createWidgetJS(css, html, js) {
|
||||||
// Use original CSS and HTML without transformation for consistency
|
// Transform the extracted JavaScript from iframe.html to work as a widget
|
||||||
const widgetCSS = css;
|
|
||||||
const widgetHTML = html;
|
// Replace the iframe's DOMContentLoaded listener with widget initialization
|
||||||
|
let transformedJS = js
|
||||||
|
// Replace the iframe class name with widget class name
|
||||||
|
.replace(/class DogCalorieCalculator/g, 'class DogCalorieCalculatorWidget')
|
||||||
|
// Replace document.getElementById with scoped selectors within the widget
|
||||||
|
.replace(/document\.getElementById\('([^']+)'\)/g, 'this.container.querySelector(\'#$1\')')
|
||||||
|
// Replace direct document queries in the class with container-scoped queries
|
||||||
|
.replace(/document\.querySelector\(/g, 'this.container.querySelector(')
|
||||||
|
.replace(/document\.querySelectorAll\(/g, 'this.container.querySelectorAll(')
|
||||||
|
// Remove the DOMContentLoaded listener and class instantiation - we'll handle this in the widget wrapper
|
||||||
|
.replace(/document\.addEventListener\('DOMContentLoaded'.*?\n.*?new DogCalorieCalculator.*?\n.*?\}\);/s, '')
|
||||||
|
// Remove duplicate theme/scale assignments that override options
|
||||||
|
.replace(/this\.theme = this\.getThemeFromURL\(\) \|\| 'system';\s*\n\s*this\.scale = this\.getScaleFromURL\(\) \|\| 1\.0;/g, '')
|
||||||
|
// Add widget initialization methods
|
||||||
|
.replace(/constructor\(\) \{/, `constructor(container, options = {}) {
|
||||||
|
this.container = container;
|
||||||
|
this.options = {
|
||||||
|
theme: options.theme || this.getThemeFromURL() || 'system',
|
||||||
|
scale: options.scale || this.getScaleFromURL() || 1.0,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
this.theme = this.options.theme;
|
||||||
|
this.scale = this.options.scale;`)
|
||||||
|
// Replace the init() method to inject HTML and apply widget settings
|
||||||
|
.replace(/init\(\) \{/, `init() {
|
||||||
|
// Inject the calculator HTML into the container
|
||||||
|
this.container.innerHTML = \`${html}\`;
|
||||||
|
|
||||||
|
// Apply widget-specific settings
|
||||||
|
this.applyTheme();
|
||||||
|
this.applyScale();
|
||||||
|
|
||||||
|
// Continue with original init logic`)
|
||||||
|
// Remove duplicate applyTheme/applyScale calls
|
||||||
|
.replace(/this\.applyTheme\(\);\s*\n\s*this\.applyScale\(\);\s*\n\s*this\.bindEvents/g, 'this.bindEvents');
|
||||||
|
|
||||||
|
// Add widget-specific methods before the class closing brace
|
||||||
|
transformedJS = transformedJS.replace(/(\s+)(\}\s*$)/, `$1
|
||||||
|
getThemeFromURL() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const theme = urlParams.get('theme');
|
||||||
|
return ['light', 'dark', 'system'].includes(theme) ? theme : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getScaleFromURL() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const scale = parseFloat(urlParams.get('scale'));
|
||||||
|
return (!isNaN(scale) && scale >= 0.5 && scale <= 2.0) ? scale : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
applyTheme() {
|
||||||
|
const calculatorContainer = this.container.querySelector('#dogCalculator');
|
||||||
|
if (calculatorContainer) {
|
||||||
|
// Remove existing theme classes
|
||||||
|
calculatorContainer.classList.remove('theme-light', 'theme-dark', 'theme-system');
|
||||||
|
// Add the selected theme class
|
||||||
|
if (['light', 'dark', 'system'].includes(this.options.theme)) {
|
||||||
|
calculatorContainer.classList.add('theme-' + this.options.theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
applyScale() {
|
||||||
|
const scale = Math.max(0.5, Math.min(2.0, this.options.scale));
|
||||||
|
if (scale !== 1.0) {
|
||||||
|
this.container.style.transform = \`scale(\${scale})\`;
|
||||||
|
this.container.style.transformOrigin = 'top left';
|
||||||
|
}
|
||||||
|
}$2`);
|
||||||
|
|
||||||
const widgetCode = `/**
|
const widgetCode = `/**
|
||||||
* Dog Calorie Calculator Widget
|
* Dog Calorie Calculator Widget
|
||||||
* Embeddable JavaScript widget for websites
|
* Embeddable JavaScript widget for websites
|
||||||
*
|
*
|
||||||
|
* THIS CODE IS AUTO-GENERATED FROM iframe.html - DO NOT EDIT MANUALLY
|
||||||
|
* Edit iframe.html and run 'node build.js' to update this file
|
||||||
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* <script src="sundog-dog-food-calculator.js"></script>
|
* <script src="sundog-dog-food-calculator.js"></script>
|
||||||
* <div id="dog-calorie-calculator"></div>
|
* <div id="dog-calorie-calculator"></div>
|
||||||
@@ -125,8 +196,8 @@ function createWidgetJS(css, html, js) {
|
|||||||
(function() {
|
(function() {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// Inject widget styles with proper namespacing
|
// Inject widget styles
|
||||||
const CSS_STYLES = \`${widgetCSS}\`;
|
const CSS_STYLES = \`${css}\`;
|
||||||
|
|
||||||
function injectStyles() {
|
function injectStyles() {
|
||||||
if (document.getElementById('dog-calculator-styles')) return;
|
if (document.getElementById('dog-calculator-styles')) return;
|
||||||
@@ -137,329 +208,8 @@ function createWidgetJS(css, html, js) {
|
|||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main widget class
|
// ACTUAL JavaScript from iframe.html (transformed for widget use)
|
||||||
class DogCalorieCalculatorWidget {
|
${transformedJS}
|
||||||
constructor(container, options = {}) {
|
|
||||||
this.container = container;
|
|
||||||
this.options = {
|
|
||||||
theme: options.theme || this.getThemeFromURL() || 'system',
|
|
||||||
scale: options.scale || this.getScaleFromURL() || 1.0,
|
|
||||||
...options
|
|
||||||
};
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
// Insert the transformed calculator HTML
|
|
||||||
this.container.innerHTML = \`${widgetHTML}\`;
|
|
||||||
|
|
||||||
// Apply widget-specific settings
|
|
||||||
this.applyTheme();
|
|
||||||
this.applyScale();
|
|
||||||
|
|
||||||
// Initialize the calculator with the same functionality as iframe
|
|
||||||
this.initCalculator();
|
|
||||||
}
|
|
||||||
|
|
||||||
initCalculator() {
|
|
||||||
const container = this.container;
|
|
||||||
|
|
||||||
// Helper functions to scope DOM queries to this widget
|
|
||||||
const $ = (selector) => container.querySelector(selector);
|
|
||||||
const $$ = (selector) => container.querySelectorAll(selector);
|
|
||||||
|
|
||||||
// Create calculator instance with exact same logic as iframe
|
|
||||||
const calc = {
|
|
||||||
currentMER: 0,
|
|
||||||
isImperial: false,
|
|
||||||
theme: this.options.theme,
|
|
||||||
scale: this.options.scale,
|
|
||||||
|
|
||||||
// Exact same calculation methods from iframe
|
|
||||||
calculateRER: (weightKg) => 70 * Math.pow(weightKg, 0.75),
|
|
||||||
calculateMER: (rer, factor) => rer * factor,
|
|
||||||
|
|
||||||
formatNumber: (num, decimals = 0) => {
|
|
||||||
if (decimals === 0) return Math.round(num).toString();
|
|
||||||
return num.toFixed(decimals).replace(/\\.?0+$/, '');
|
|
||||||
},
|
|
||||||
|
|
||||||
validateInput: (value, min = 0, isInteger = false) => {
|
|
||||||
const num = parseFloat(value);
|
|
||||||
if (isNaN(num) || num < min) return false;
|
|
||||||
if (isInteger && !Number.isInteger(num)) return false;
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
convertUnits: (grams, unit) => {
|
|
||||||
switch (unit) {
|
|
||||||
case 'kg': return grams / 1000;
|
|
||||||
case 'oz': return grams / 28.3495;
|
|
||||||
case 'lb': return grams / 453.592;
|
|
||||||
default: return grams;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getWeightInKg: () => {
|
|
||||||
const weightInput = $('#weight');
|
|
||||||
if (!weightInput || !weightInput.value) return null;
|
|
||||||
const weight = parseFloat(weightInput.value);
|
|
||||||
if (isNaN(weight)) return null;
|
|
||||||
return calc.isImperial ? weight / 2.20462 : weight;
|
|
||||||
},
|
|
||||||
|
|
||||||
getFoodEnergyPer100g: () => {
|
|
||||||
const foodEnergyInput = $('#foodEnergy');
|
|
||||||
const energyUnitSelect = $('#energyUnit');
|
|
||||||
if (!foodEnergyInput || !foodEnergyInput.value || !energyUnitSelect) return null;
|
|
||||||
|
|
||||||
const energy = parseFloat(foodEnergyInput.value);
|
|
||||||
if (isNaN(energy)) return null;
|
|
||||||
|
|
||||||
const unit = energyUnitSelect.value;
|
|
||||||
switch (unit) {
|
|
||||||
case 'kcal100g': return energy;
|
|
||||||
case 'kcalkg': return energy / 10;
|
|
||||||
case 'kcalcup': return energy / 1.2;
|
|
||||||
case 'kcalcan': return energy / 4.5;
|
|
||||||
default: return energy;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
showError: (elementId, show = true) => {
|
|
||||||
const errorElement = $('#' + elementId);
|
|
||||||
if (errorElement) {
|
|
||||||
if (show) {
|
|
||||||
errorElement.classList.remove('dog-calculator-hidden');
|
|
||||||
} else {
|
|
||||||
errorElement.classList.add('dog-calculator-hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateCalorieCalculations: () => {
|
|
||||||
const dogTypeSelect = $('#dogType');
|
|
||||||
const calorieResults = $('#calorieResults');
|
|
||||||
const rerValue = $('#rerValue');
|
|
||||||
const merValue = $('#merValue');
|
|
||||||
|
|
||||||
if (!dogTypeSelect || !calorieResults || !rerValue || !merValue) return;
|
|
||||||
|
|
||||||
const weightKg = calc.getWeightInKg();
|
|
||||||
const dogTypeFactor = dogTypeSelect.value;
|
|
||||||
|
|
||||||
calc.showError('weightError', false);
|
|
||||||
|
|
||||||
if (!weightKg || weightKg < 0.1) {
|
|
||||||
const weightInput = $('#weight');
|
|
||||||
if (weightInput && weightInput.value) calc.showError('weightError', true);
|
|
||||||
calorieResults.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dogTypeFactor) {
|
|
||||||
calorieResults.style.display = 'none';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const factor = parseFloat(dogTypeFactor);
|
|
||||||
const rer = calc.calculateRER(weightKg);
|
|
||||||
const mer = calc.calculateMER(rer, factor);
|
|
||||||
|
|
||||||
calc.currentMER = mer;
|
|
||||||
|
|
||||||
rerValue.textContent = calc.formatNumber(rer, 0) + ' cal/day';
|
|
||||||
merValue.textContent = calc.formatNumber(mer, 0) + ' cal/day';
|
|
||||||
calorieResults.style.display = 'block';
|
|
||||||
|
|
||||||
calc.updateFoodCalculations();
|
|
||||||
},
|
|
||||||
|
|
||||||
updateFoodCalculations: () => {
|
|
||||||
if (calc.currentMER === 0) return;
|
|
||||||
|
|
||||||
const daysInput = $('#days');
|
|
||||||
const unitSelect = $('#unit');
|
|
||||||
const dailyFoodResults = $('#dailyFoodResults');
|
|
||||||
const dailyFoodValue = $('#dailyFoodValue');
|
|
||||||
const totalFoodDisplay = $('#totalFoodDisplay');
|
|
||||||
|
|
||||||
if (!daysInput || !unitSelect || !dailyFoodResults || !dailyFoodValue || !totalFoodDisplay) return;
|
|
||||||
|
|
||||||
const energyPer100g = calc.getFoodEnergyPer100g();
|
|
||||||
const days = daysInput.value;
|
|
||||||
const unit = unitSelect.value;
|
|
||||||
|
|
||||||
calc.showError('foodEnergyError', false);
|
|
||||||
calc.showError('daysError', false);
|
|
||||||
|
|
||||||
if (!energyPer100g || energyPer100g < 0.1) {
|
|
||||||
const foodEnergyInput = $('#foodEnergy');
|
|
||||||
if (foodEnergyInput && foodEnergyInput.value) calc.showError('foodEnergyError', true);
|
|
||||||
dailyFoodResults.style.display = 'none';
|
|
||||||
totalFoodDisplay.value = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!days || !calc.validateInput(days, 1, true)) {
|
|
||||||
if (days) calc.showError('daysError', true);
|
|
||||||
totalFoodDisplay.value = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const numDays = parseInt(days);
|
|
||||||
const dailyFoodGrams = (calc.currentMER / energyPer100g) * 100;
|
|
||||||
const totalFoodGrams = dailyFoodGrams * numDays;
|
|
||||||
|
|
||||||
dailyFoodValue.textContent = calc.formatNumber(dailyFoodGrams, 1) + ' g/day';
|
|
||||||
dailyFoodResults.style.display = 'block';
|
|
||||||
|
|
||||||
const convertedAmount = calc.convertUnits(totalFoodGrams, unit);
|
|
||||||
const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : 'lb';
|
|
||||||
const decimals = unit === 'g' ? 0 : unit === 'kg' ? 2 : 1;
|
|
||||||
|
|
||||||
totalFoodDisplay.value = calc.formatNumber(convertedAmount, decimals) + ' ' + unitLabel;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Unit switching methods (missing from previous version!)
|
|
||||||
toggleUnits: () => {
|
|
||||||
const toggle = $('#unitToggle');
|
|
||||||
calc.isImperial = toggle.checked;
|
|
||||||
|
|
||||||
calc.updateUnitLabels();
|
|
||||||
calc.convertExistingValues();
|
|
||||||
calc.updateCalorieCalculations();
|
|
||||||
},
|
|
||||||
|
|
||||||
updateUnitLabels: () => {
|
|
||||||
const metricLabel = $('#metricLabel');
|
|
||||||
const imperialLabel = $('#imperialLabel');
|
|
||||||
const weightLabel = $('#weightLabel');
|
|
||||||
const weightInput = $('#weight');
|
|
||||||
const unitSelect = $('#unit');
|
|
||||||
const energyUnitSelect = $('#energyUnit');
|
|
||||||
|
|
||||||
if (metricLabel && imperialLabel) {
|
|
||||||
metricLabel.classList.toggle('active', !calc.isImperial);
|
|
||||||
imperialLabel.classList.toggle('active', calc.isImperial);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (calc.isImperial) {
|
|
||||||
if (weightLabel) weightLabel.textContent = "Dog's Weight (lbs):";
|
|
||||||
if (weightInput) {
|
|
||||||
weightInput.placeholder = "Enter weight in lbs";
|
|
||||||
weightInput.min = "0.2";
|
|
||||||
weightInput.step = "0.1";
|
|
||||||
}
|
|
||||||
if (unitSelect) {
|
|
||||||
unitSelect.innerHTML = '<option value="oz">ounces (oz)</option>' +
|
|
||||||
'<option value="lb">pounds (lb)</option>' +
|
|
||||||
'<option value="g">grams (g)</option>' +
|
|
||||||
'<option value="kg">kilograms (kg)</option>';
|
|
||||||
}
|
|
||||||
// Set energy unit to kcal/cup for imperial
|
|
||||||
if (energyUnitSelect && energyUnitSelect.value === 'kcal100g') {
|
|
||||||
energyUnitSelect.value = 'kcalcup';
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (weightLabel) weightLabel.textContent = "Dog's Weight (kg):";
|
|
||||||
if (weightInput) {
|
|
||||||
weightInput.placeholder = "Enter weight in kg";
|
|
||||||
weightInput.min = "0.1";
|
|
||||||
weightInput.step = "0.1";
|
|
||||||
}
|
|
||||||
if (unitSelect) {
|
|
||||||
unitSelect.innerHTML = '<option value="g">grams (g)</option>' +
|
|
||||||
'<option value="kg">kilograms (kg)</option>' +
|
|
||||||
'<option value="oz">ounces (oz)</option>' +
|
|
||||||
'<option value="lb">pounds (lb)</option>';
|
|
||||||
}
|
|
||||||
// Set energy unit to kcal/100g for metric
|
|
||||||
if (energyUnitSelect && energyUnitSelect.value === 'kcalcup') {
|
|
||||||
energyUnitSelect.value = 'kcal100g';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
convertExistingValues: () => {
|
|
||||||
const weightInput = $('#weight');
|
|
||||||
|
|
||||||
if (weightInput && weightInput.value) {
|
|
||||||
const currentWeight = parseFloat(weightInput.value);
|
|
||||||
if (!isNaN(currentWeight)) {
|
|
||||||
if (calc.isImperial) {
|
|
||||||
weightInput.value = calc.formatNumber(currentWeight * 2.20462, 1);
|
|
||||||
} else {
|
|
||||||
weightInput.value = calc.formatNumber(currentWeight / 2.20462, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
bindEvents: () => {
|
|
||||||
const weightInput = $('#weight');
|
|
||||||
const dogTypeSelect = $('#dogType');
|
|
||||||
const foodEnergyInput = $('#foodEnergy');
|
|
||||||
const daysInput = $('#days');
|
|
||||||
const unitSelect = $('#unit');
|
|
||||||
const energyUnitSelect = $('#energyUnit');
|
|
||||||
const unitToggle = $('#unitToggle');
|
|
||||||
|
|
||||||
if (weightInput) {
|
|
||||||
weightInput.addEventListener('input', () => calc.updateCalorieCalculations());
|
|
||||||
}
|
|
||||||
if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => calc.updateCalorieCalculations());
|
|
||||||
if (foodEnergyInput) {
|
|
||||||
foodEnergyInput.addEventListener('input', () => calc.updateFoodCalculations());
|
|
||||||
}
|
|
||||||
if (energyUnitSelect) energyUnitSelect.addEventListener('change', () => calc.updateFoodCalculations());
|
|
||||||
if (daysInput) {
|
|
||||||
daysInput.addEventListener('input', () => calc.updateFoodCalculations());
|
|
||||||
}
|
|
||||||
if (unitSelect) unitSelect.addEventListener('change', () => calc.updateFoodCalculations());
|
|
||||||
if (unitToggle) unitToggle.addEventListener('change', () => calc.toggleUnits());
|
|
||||||
},
|
|
||||||
|
|
||||||
init: () => {
|
|
||||||
calc.bindEvents();
|
|
||||||
calc.updateUnitLabels(); // Initialize unit labels
|
|
||||||
const calcContainer = $('#dogCalculator');
|
|
||||||
if (calcContainer) {
|
|
||||||
calcContainer.classList.add('loaded');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
calc.init();
|
|
||||||
return calc;
|
|
||||||
}
|
|
||||||
|
|
||||||
getThemeFromURL() {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const theme = urlParams.get('theme');
|
|
||||||
return ['light', 'dark', 'system'].includes(theme) ? theme : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
getScaleFromURL() {
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
|
||||||
const scale = parseFloat(urlParams.get('scale'));
|
|
||||||
return (!isNaN(scale) && scale >= 0.5 && scale <= 2.0) ? scale : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
applyTheme() {
|
|
||||||
if (this.options.theme === 'light' || this.options.theme === 'dark') {
|
|
||||||
this.container.classList.add('theme-' + this.options.theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applyScale() {
|
|
||||||
const scale = Math.max(0.5, Math.min(2.0, this.options.scale));
|
|
||||||
if (scale !== 1.0) {
|
|
||||||
this.container.style.transform = \`scale(\${scale})\`;
|
|
||||||
this.container.style.transformOrigin = 'top left';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-initialize widgets on page load
|
// Auto-initialize widgets on page load
|
||||||
function initializeWidget() {
|
function initializeWidget() {
|
||||||
|
|||||||
+2087
-307
File diff suppressed because it is too large
Load Diff
+2559
-490
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -24,12 +24,12 @@
|
|||||||
|
|
||||||
<div class="test-container">
|
<div class="test-container">
|
||||||
<h2>Test 1: Basic Widget</h2>
|
<h2>Test 1: Basic Widget</h2>
|
||||||
<div id="dog-calorie-calculator"></div>
|
<div class="dog-calorie-calculator" data-theme="light" data-scale="0.9"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="test-container">
|
<div class="test-container">
|
||||||
<h2>Test 2: Dark Theme Widget</h2>
|
<h2>Test 2: Dark Theme Widget</h2>
|
||||||
<div id="dog-calorie-calculator" data-theme="dark" data-scale="0.5"></div>
|
<div class="dog-calorie-calculator" data-theme="dark" data-scale="0.5"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="sundog-dog-food-calculator.js"></script>
|
<script src="sundog-dog-food-calculator.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user