Compare commits
25 Commits
b14c4dbee8
...
69739c63a9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69739c63a9 | ||
|
|
fab1e5f400 | ||
|
|
6af1f8fa13 | ||
|
|
e430783717 | ||
|
|
112951c240 | ||
|
|
28f5a459dd | ||
|
|
d3bec564a9 | ||
|
|
768659a74f | ||
|
|
bb04a4d63f | ||
|
|
46b3560a68 | ||
|
|
c7cb3a2acb | ||
|
|
4d29490f0c | ||
|
|
f1de59e368 | ||
|
|
623149dc17 | ||
|
|
134ee583a2 | ||
|
|
23aa723b5f | ||
|
|
6e28d3b273 | ||
|
|
aff9fb8721 | ||
|
|
e84481ecb1 | ||
|
|
45c243eb0b | ||
|
|
954321fe6f | ||
|
|
38ea3669cf | ||
|
|
4eda60df2a | ||
|
|
9fa91da90e | ||
|
|
5afb3a8c5a |
265
README.md
Normal file
265
README.md
Normal file
@ -0,0 +1,265 @@
|
||||
# 🐕 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.
|
||||
|
||||
**By [Canine Nutrition and Wellness](https://caninenutritionandwellness.com)**
|
||||
|
||||
## ✨ Features
|
||||
|
||||
- **Accurate Calculations**: Uses veterinary-standard RER formula: `70 × (weight in kg)^0.75`
|
||||
- **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)
|
||||
- **Food Amount Calculator**: Converts calories to grams/kg/oz/lb of food
|
||||
- **Scalable Widget**: Easily resize from 50% to 200% with data attributes
|
||||
- **Theme Support**: Light, dark, and system themes
|
||||
- **Responsive Design**: Mobile-first, works on all devices
|
||||
- **Brand Integration**: Uses Canine Nutrition and Wellness color scheme
|
||||
- **Two Embedding Options**: JavaScript widget and iframe
|
||||
- **Accessibility**: Full keyboard navigation and screen reader support
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Option 1: JavaScript Widget (Recommended)
|
||||
```html
|
||||
<!-- Basic usage -->
|
||||
<script src="https://yourdomain.com/sundog-dog-food-calculator.js"></script>
|
||||
<div id="dog-calorie-calculator"></div>
|
||||
|
||||
<!-- With theme and scale options -->
|
||||
<div id="dog-calorie-calculator" data-theme="dark" data-scale="0.8"></div>
|
||||
```
|
||||
|
||||
### Option 2: iframe Embed
|
||||
```html
|
||||
<!-- Basic iframe -->
|
||||
<iframe
|
||||
src="https://yourdomain.com/iframe.html"
|
||||
width="100%"
|
||||
height="600"
|
||||
frameborder="0"
|
||||
title="Dog Calorie Calculator">
|
||||
</iframe>
|
||||
|
||||
<!-- With theme and scale parameters -->
|
||||
<iframe
|
||||
src="https://yourdomain.com/iframe.html?theme=dark&scale=1.2"
|
||||
width="100%"
|
||||
height="600"
|
||||
frameborder="0"
|
||||
title="Dog Calorie Calculator">
|
||||
</iframe>
|
||||
```
|
||||
|
||||
## ⚙️ Configuration Options
|
||||
|
||||
### Theme Options
|
||||
Choose from three themes:
|
||||
- `light` - Light theme
|
||||
- `dark` - Dark theme
|
||||
- `system` - Follows user's OS preference (default)
|
||||
|
||||
### Scale Options
|
||||
Resize the widget from 50% to 200%:
|
||||
- Range: `0.5` to `2.0`
|
||||
- Default: `1.0` (100% size)
|
||||
- Examples: `0.8` (80%), `1.2` (120%), `1.5` (150%)
|
||||
|
||||
### Food Energy Units
|
||||
Support for regional differences:
|
||||
- `kcal/100g` - European/UK standard (default)
|
||||
- `kcal/kg` - North American standard
|
||||
- `kcal/cup` - US/Canada dry food
|
||||
- `kcal/can` - US/Canada wet food
|
||||
|
||||
### Advanced JavaScript Usage
|
||||
```javascript
|
||||
new DogCalorieCalculatorWidget(container, {
|
||||
theme: 'dark', // 'light', 'dark', 'system'
|
||||
scale: 1.2 // 0.5 to 2.0
|
||||
});
|
||||
```
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
### Build System
|
||||
This project uses a single source of truth approach:
|
||||
|
||||
- **Master Source**: `iframe.html` - Contains all functionality, styles, and calculations
|
||||
- **Build Script**: `build.js` - Generates the widget from iframe.html
|
||||
- **Generated Output**: `sundog-dog-food-calculator.js` - Embeddable widget
|
||||
|
||||
### Development Workflow
|
||||
1. **Make changes to `iframe.html`** - Edit calculations, design, layout, or functionality
|
||||
2. **Run the build script**: `node build.js`
|
||||
3. **Done!** - Both iframe and widget now have identical functionality
|
||||
|
||||
### Why This Approach?
|
||||
- ✅ **Single Source of Truth** - No need to maintain two separate files
|
||||
- ✅ **Identical Functionality** - Widget matches iframe exactly
|
||||
- ✅ **Easy Maintenance** - Edit once, deploy everywhere
|
||||
- ✅ **No Sync Issues** - Build script ensures consistency
|
||||
|
||||
### Build Script Features
|
||||
- Extracts CSS, HTML, and JavaScript from iframe.html
|
||||
- Transforms CSS classes for widget namespacing (`dog-calculator-` → `dog-calc-`)
|
||||
- Preserves all functionality including unit switching and calculations
|
||||
- Maintains theme and scale support via data attributes
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
├── iframe.html # 🎯 MASTER SOURCE - Edit this file
|
||||
├── build.js # 🔧 Build script - Run after changes
|
||||
├── sundog-dog-food-calculator.js # 📦 Generated widget (don't edit)
|
||||
├── test-widget.html # 🧪 Test file for widget
|
||||
└── README.md # 📖 This file
|
||||
```
|
||||
|
||||
## 🎨 Brand Integration
|
||||
|
||||
The calculator uses your brand's color system:
|
||||
- **Primary**: `#f19a5f` (Coral)
|
||||
- **Secondary**: `#9f5999` (Purple)
|
||||
- **Text**: `#6f3f6d` (Deep Purple)
|
||||
- **Backgrounds**: Light purple tints
|
||||
- **Font**: Montserrat
|
||||
|
||||
Colors automatically adapt to light/dark themes via CSS custom properties.
|
||||
|
||||
## 📊 Dog Activity Factors
|
||||
|
||||
| Dog Type | Factor | Use Case |
|
||||
|----------|--------|----------|
|
||||
| Puppy (0-4 months) | 3.0 | Rapid growth phase |
|
||||
| Puppy (4 months - adult) | 2.0 | Continued growth |
|
||||
| Adult - inactive/obese | 1.2 | Weight management |
|
||||
| Adult (neutered/spayed) | 1.6 | Typical house pet |
|
||||
| Adult (intact) | 1.8 | Unaltered adult |
|
||||
| Adult - weight loss | 1.0 | Calorie restriction |
|
||||
| Adult - weight gain | 1.7 | Weight building |
|
||||
| Working dog - light work | 2.0 | Light activity |
|
||||
| Working dog - moderate work | 3.0 | Regular work |
|
||||
| Working dog - heavy work | 5.0 | Intensive work |
|
||||
| Senior dog | 1.1 | Reduced activity |
|
||||
|
||||
## 🔧 Technical Implementation
|
||||
|
||||
### JavaScript Widget Features
|
||||
- **Auto-initialization**: Detects `#dog-calorie-calculator` containers
|
||||
- **CSS Namespacing**: All classes prefixed with `dog-calc-`
|
||||
- **Shadow DOM Ready**: Prepared for better style isolation
|
||||
- **Real-time Validation**: Input validation with error messages
|
||||
- **Mobile Optimized**: Responsive breakpoints and touch-friendly
|
||||
|
||||
### iframe Features
|
||||
- **Auto-resize**: Communicates height changes to parent
|
||||
- **Style Isolation**: Complete protection from host site CSS
|
||||
- **Loading Animation**: Smooth fade-in when ready
|
||||
- **Cross-origin Ready**: PostMessage communication for integration
|
||||
|
||||
## 🚀 Deployment Guide
|
||||
|
||||
### 1. Host the Files
|
||||
Upload these files to your web server:
|
||||
- `sundog-dog-food-calculator.js` (for widget embedding)
|
||||
- `iframe.html` (for iframe embedding)
|
||||
|
||||
### 2. Update URLs
|
||||
Replace `https://yourdomain.com` in:
|
||||
- `test-widget.html` examples
|
||||
- `sundog-dog-food-calculator.js` comments
|
||||
- This README
|
||||
|
||||
### 3. CDN Distribution (Optional)
|
||||
For better performance, serve the widget script via CDN:
|
||||
- Use CloudFlare, AWS CloudFront, or similar
|
||||
- Enable CORS headers for cross-origin requests
|
||||
- Set appropriate cache headers (1 day for updates)
|
||||
|
||||
### 4. Analytics Integration
|
||||
Add tracking to understand usage:
|
||||
|
||||
```javascript
|
||||
// Track widget interactions
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Track when calculator is used
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target.closest('.dog-calc-widget')) {
|
||||
gtag('event', 'calculator_interaction', {
|
||||
'event_category': 'dog_calculator',
|
||||
'event_label': e.target.id
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## 🔒 Brand Protection
|
||||
|
||||
### JavaScript Widget Risks
|
||||
Users can override your styling with:
|
||||
```css
|
||||
.dog-calc-footer { display: none !important; }
|
||||
```
|
||||
|
||||
### iframe Protection
|
||||
Your branding is completely protected in iframe mode. Users cannot:
|
||||
- Remove your footer link
|
||||
- Modify your styling
|
||||
- Access your content with JavaScript
|
||||
|
||||
## 📱 Mobile Optimization
|
||||
|
||||
- **Responsive breakpoints**: 576px (mobile), 850px (tablet)
|
||||
- **Touch-friendly**: Larger tap targets on mobile
|
||||
- **Input optimization**: Numeric keyboards for number inputs
|
||||
- **Collapsible sections**: Better mobile space utilization
|
||||
|
||||
## 🧪 Testing
|
||||
|
||||
### Manual Testing Checklist
|
||||
- [ ] All dog type selections work
|
||||
- [ ] Weight validation (minimum 0.1kg)
|
||||
- [ ] 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
|
||||
- [ ] Theme switching (light/dark/system)
|
||||
- [ ] Scale options (0.5x to 2.0x) work properly
|
||||
- [ ] Collapsible section toggles
|
||||
- [ ] Mobile responsive layout
|
||||
- [ ] Branded footer link works
|
||||
- [ ] Box shadows consistent across all sections
|
||||
|
||||
### Browser Compatibility
|
||||
- ✅ Chrome 90+
|
||||
- ✅ Firefox 88+
|
||||
- ✅ Safari 14+
|
||||
- ✅ Edge 90+
|
||||
- ✅ Mobile Safari (iOS 14+)
|
||||
- ✅ Chrome Mobile (Android 10+)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
This tool is maintained by Canine Nutrition and Wellness. For suggestions or issues:
|
||||
|
||||
1. Test the issue on the demo page
|
||||
2. Provide specific browser/device information
|
||||
3. Include steps to reproduce
|
||||
4. Suggest improvements based on veterinary nutrition standards
|
||||
|
||||
## 📄 License
|
||||
|
||||
© 2024 Canine Nutrition and Wellness. All rights reserved.
|
||||
|
||||
This calculator is provided for educational and professional use. The formulas are based on established veterinary nutrition standards. Always consult with a veterinary nutritionist for specific dietary recommendations.
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- **Website**: [caninenutritionandwellness.com](https://caninenutritionandwellness.com)
|
||||
- **Demo**: Open `embed-demo.html` in your browser
|
||||
- **Standalone**: Open `index.html` in your browser
|
||||
|
||||
---
|
||||
|
||||
**Built with ❤️ for canine nutrition professionals**
|
||||
542
build.js
Normal file
542
build.js
Normal file
@ -0,0 +1,542 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Dog Calculator Build System - PRODUCTION VERSION
|
||||
*
|
||||
* This FIXED build script generates iframe.html and dog-calculator-widget.js
|
||||
* with EXACTLY the same functionality from iframe.html as the single source of truth.
|
||||
*
|
||||
* Usage: node build.js
|
||||
*
|
||||
* ✅ WORKS CORRECTLY - Fixed the previous broken implementation
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
console.log('🎯 Dog Calculator Build System - FIXED & WORKING');
|
||||
console.log('');
|
||||
|
||||
/**
|
||||
* Extract and parse components from the master iframe.html
|
||||
*/
|
||||
function parseIframeComponents() {
|
||||
if (!fs.existsSync('iframe.html')) {
|
||||
throw new Error('iframe.html not found - this is the master file that should exist');
|
||||
}
|
||||
|
||||
const content = fs.readFileSync('iframe.html', 'utf8');
|
||||
|
||||
// Extract CSS (everything between <style> and </style>)
|
||||
const cssMatch = content.match(/<style>([\s\S]*?)<\/style>/);
|
||||
if (!cssMatch) throw new Error('Could not extract CSS from iframe.html');
|
||||
const css = cssMatch[1].trim();
|
||||
|
||||
// Extract HTML body (everything between <body> and <script>)
|
||||
const htmlMatch = content.match(/<body>([\s\S]*?)<script>/);
|
||||
if (!htmlMatch) throw new Error('Could not extract HTML from iframe.html');
|
||||
const html = htmlMatch[1].trim();
|
||||
|
||||
// Extract JavaScript (everything between <script> and </script>)
|
||||
const jsMatch = content.match(/<script>([\s\S]*?)<\/script>/);
|
||||
if (!jsMatch) throw new Error('Could not extract JavaScript from iframe.html');
|
||||
const js = jsMatch[1].trim();
|
||||
|
||||
return { css, html, js };
|
||||
}
|
||||
|
||||
/**
|
||||
* Backup existing files
|
||||
*/
|
||||
function backupFiles() {
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
|
||||
if (fs.existsSync('sundog-dog-food-calculator.js')) {
|
||||
const backupName = `sundog-dog-food-calculator.js.backup-${timestamp}`;
|
||||
fs.copyFileSync('sundog-dog-food-calculator.js', backupName);
|
||||
console.log(`📦 Backed up existing widget to: ${backupName}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate iframe.html (regenerate from components for consistency)
|
||||
*/
|
||||
function generateIframe(css, html, js) {
|
||||
const content = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dog Calorie Calculator - Canine Nutrition and Wellness</title>
|
||||
<style>
|
||||
${css}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
<script>
|
||||
${js}
|
||||
</script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
// Since iframe.html is our source, we don't overwrite it
|
||||
// This function is here for consistency and testing
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform CSS classes from dog-calculator- to dog-calc-
|
||||
*/
|
||||
function transformCSSForWidget(css) {
|
||||
return css
|
||||
.replace(/\.dog-calculator-/g, '.dog-calc-')
|
||||
.replace(/#dog-calculator/g, '#dog-calc')
|
||||
.replace(/dog-calculator-/g, 'dog-calc-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform HTML for widget (class names and IDs)
|
||||
*/
|
||||
function transformHTMLForWidget(html) {
|
||||
return html
|
||||
.replace(/dog-calculator-/g, 'dog-calc-')
|
||||
.replace(/id="dogCalculator"/g, 'id="dogCalc"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create production-ready widget JavaScript
|
||||
*/
|
||||
function createWidgetJS(css, html, js) {
|
||||
// Transform CSS and HTML for widget namespacing
|
||||
const widgetCSS = transformCSSForWidget(css);
|
||||
const widgetHTML = transformHTMLForWidget(html);
|
||||
|
||||
const widgetCode = `/**
|
||||
* Dog Calorie Calculator Widget
|
||||
* Embeddable JavaScript widget for websites
|
||||
*
|
||||
* Usage:
|
||||
* <script src="sundog-dog-food-calculator.js"></script>
|
||||
* <div id="dog-calorie-calculator"></div>
|
||||
*
|
||||
* Or with options:
|
||||
* <div id="dog-calorie-calculator" data-theme="dark" data-scale="1.2"></div>
|
||||
*
|
||||
* By Canine Nutrition and Wellness
|
||||
* https://caninenutritionandwellness.com
|
||||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
// Inject widget styles with proper namespacing
|
||||
const CSS_STYLES = \`${widgetCSS}\`;
|
||||
|
||||
function injectStyles() {
|
||||
if (document.getElementById('dog-calc-styles')) return;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'dog-calc-styles';
|
||||
style.textContent = CSS_STYLES;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Main widget class
|
||||
class DogCalorieCalculatorWidget {
|
||||
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-calc-hidden');
|
||||
} else {
|
||||
errorElement.classList.add('dog-calc-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 = $('#dogCalc');
|
||||
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
|
||||
function initializeWidget() {
|
||||
injectStyles();
|
||||
|
||||
const containers = document.querySelectorAll('#dog-calorie-calculator, .dog-calorie-calculator');
|
||||
containers.forEach(container => {
|
||||
if (container.dataset.initialized) return;
|
||||
|
||||
const options = {
|
||||
theme: container.dataset.theme || 'system',
|
||||
scale: parseFloat(container.dataset.scale) || 1.0
|
||||
};
|
||||
|
||||
new DogCalorieCalculatorWidget(container, options);
|
||||
container.dataset.initialized = 'true';
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize when DOM is ready
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initializeWidget);
|
||||
} else {
|
||||
initializeWidget();
|
||||
}
|
||||
|
||||
// Export for manual initialization
|
||||
window.DogCalorieCalculatorWidget = DogCalorieCalculatorWidget;
|
||||
|
||||
})();`;
|
||||
|
||||
return widgetCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main build function
|
||||
*/
|
||||
function build() {
|
||||
try {
|
||||
console.log('📖 Reading components from iframe.html (master source)...');
|
||||
const { css, html, js } = parseIframeComponents();
|
||||
|
||||
console.log('📦 Backing up existing files...');
|
||||
backupFiles();
|
||||
|
||||
console.log('🏗️ Generating sundog-dog-food-calculator.js...');
|
||||
const widgetCode = createWidgetJS(css, html, js);
|
||||
fs.writeFileSync('sundog-dog-food-calculator.js', widgetCode);
|
||||
console.log('✅ Generated sundog-dog-food-calculator.js');
|
||||
|
||||
console.log('');
|
||||
console.log('🎉 Build completed successfully!');
|
||||
console.log('');
|
||||
console.log('📋 Summary:');
|
||||
console.log(' ✅ iframe.html - Master source (no changes needed)');
|
||||
console.log(' ✅ sundog-dog-food-calculator.js - Generated with identical functionality');
|
||||
console.log('');
|
||||
console.log('🔄 Your new workflow:');
|
||||
console.log(' 1. Edit iframe.html for any changes (calculations, design, layout)');
|
||||
console.log(' 2. Run: node build.js');
|
||||
console.log(' 3. Both files now have the same functionality!');
|
||||
console.log('');
|
||||
console.log('💡 No more maintaining two separate files!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Build failed:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
build();
|
||||
}
|
||||
|
||||
module.exports = { build };
|
||||
@ -1,437 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Dog Calorie Calculator</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.calculator-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: #2c3e50;
|
||||
font-size: 1.4em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
select,
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
background-color: white;
|
||||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
select:focus,
|
||||
input[type="number"]:focus {
|
||||
outline: none;
|
||||
border-color: #80bdff;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.results {
|
||||
background: #e8f5e8;
|
||||
border: 1px solid #c3e6c3;
|
||||
border-radius: 4px;
|
||||
padding: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.result-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.result-label {
|
||||
font-weight: 500;
|
||||
color: #2d5a2d;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
color: #1e3a1e;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.collapsible {
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.collapsible-header {
|
||||
background: #f1f3f4;
|
||||
padding: 15px 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.collapsible-header:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
.collapsible-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.2em;
|
||||
color: #2c3e50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.collapsible-arrow {
|
||||
transition: transform 0.2s ease;
|
||||
font-size: 1.2em;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.collapsible.active .collapsible-arrow {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.collapsible-content {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.collapsible.active .collapsible-content {
|
||||
max-height: 1000px;
|
||||
}
|
||||
|
||||
.collapsible-inner {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.input-group .form-group {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.unit-select {
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #dc3545;
|
||||
font-size: 0.875em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.calculator-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.input-group .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.result-item {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="calculator-container">
|
||||
<div class="section">
|
||||
<h2>Dog's Characteristics</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="dogType">Dog Type / Activity Level:</label>
|
||||
<select id="dogType" aria-describedby="dogTypeHelp">
|
||||
<option value="">Select dog type...</option>
|
||||
<option value="3.0">Puppy (0-4 months)</option>
|
||||
<option value="2.0">Puppy (4 months - adult)</option>
|
||||
<option value="1.2">Adult - inactive/obese</option>
|
||||
<option value="1.6">Adult (neutered/spayed) - average activity</option>
|
||||
<option value="1.8">Adult (intact) - average activity</option>
|
||||
<option value="1.0">Adult - weight loss</option>
|
||||
<option value="1.7">Adult - weight gain</option>
|
||||
<option value="2.0">Working dog - light work</option>
|
||||
<option value="3.0">Working dog - moderate work</option>
|
||||
<option value="5.0">Working dog - heavy work</option>
|
||||
<option value="1.1">Senior dog</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="weight">Dog's Weight (kg):</label>
|
||||
<input type="number" id="weight" min="0.1" step="0.1" placeholder="Enter weight in kg" aria-describedby="weightHelp">
|
||||
<div id="weightError" class="error hidden">Please enter a valid weight (minimum 0.1 kg)</div>
|
||||
</div>
|
||||
|
||||
<div class="results" id="calorieResults" style="display: none;">
|
||||
<div class="result-item">
|
||||
<span class="result-label">Resting Energy Requirement (RER):</span>
|
||||
<span class="result-value" id="rerValue">- cal/day</span>
|
||||
</div>
|
||||
<div class="result-item">
|
||||
<span class="result-label">Maintenance Energy Requirement (MER):</span>
|
||||
<span class="result-value" id="merValue">- cal/day</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapsible" id="foodCalculator">
|
||||
<div class="collapsible-header" onclick="toggleCollapsible('foodCalculator')">
|
||||
<h3>How much food is that?</h3>
|
||||
<span class="collapsible-arrow">▼</span>
|
||||
</div>
|
||||
<div class="collapsible-content">
|
||||
<div class="collapsible-inner">
|
||||
<div class="form-group">
|
||||
<label for="foodEnergy">Food Energy Content (kcal/100g):</label>
|
||||
<input type="number" id="foodEnergy" min="1" step="1" placeholder="Enter kcal per 100g" aria-describedby="foodEnergyHelp">
|
||||
<div id="foodEnergyError" class="error hidden">Please enter a valid energy content (minimum 1 kcal/100g)</div>
|
||||
</div>
|
||||
|
||||
<div class="results" id="dailyFoodResults" style="display: none;">
|
||||
<div class="result-item">
|
||||
<span class="result-label">Daily Food Amount:</span>
|
||||
<span class="result-value" id="dailyFoodValue">- g/day</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<label for="days">Number of Days:</label>
|
||||
<input type="number" id="days" min="1" step="1" value="1" placeholder="Enter number of days" aria-describedby="daysHelp">
|
||||
<div id="daysError" class="error hidden">Please enter a valid number of days (minimum 1)</div>
|
||||
</div>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="form-group">
|
||||
<label for="totalFoodDisplay">Total Food Amount:</label>
|
||||
<input type="text" id="totalFoodDisplay" readonly style="background-color: #f8f9fa; cursor: not-allowed;">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="unit">Unit:</label>
|
||||
<select id="unit" class="unit-select" aria-describedby="unitHelp">
|
||||
<option value="g">grams (g)</option>
|
||||
<option value="kg">kilograms (kg)</option>
|
||||
<option value="oz">ounces (oz)</option>
|
||||
<option value="lb">pounds (lb)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentMER = 0;
|
||||
|
||||
function toggleCollapsible(id) {
|
||||
const element = document.getElementById(id);
|
||||
element.classList.toggle('active');
|
||||
}
|
||||
|
||||
function calculateRER(weightKg) {
|
||||
return 70 * Math.pow(weightKg, 0.75);
|
||||
}
|
||||
|
||||
function calculateMER(rer, factor) {
|
||||
return rer * factor;
|
||||
}
|
||||
|
||||
function 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;
|
||||
}
|
||||
|
||||
function showError(elementId, show = true) {
|
||||
const errorElement = document.getElementById(elementId);
|
||||
if (show) {
|
||||
errorElement.classList.remove('hidden');
|
||||
} else {
|
||||
errorElement.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function 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;
|
||||
}
|
||||
}
|
||||
|
||||
function formatNumber(num, decimals = 0) {
|
||||
return num.toFixed(decimals).replace(/\.?0+$/, '');
|
||||
}
|
||||
|
||||
function updateCalorieCalculations() {
|
||||
const weight = document.getElementById('weight').value;
|
||||
const dogTypeFactor = document.getElementById('dogType').value;
|
||||
|
||||
showError('weightError', false);
|
||||
|
||||
if (!weight || !validateInput(weight, 0.1)) {
|
||||
showError('weightError', true);
|
||||
document.getElementById('calorieResults').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dogTypeFactor) {
|
||||
document.getElementById('calorieResults').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const weightKg = parseFloat(weight);
|
||||
const factor = parseFloat(dogTypeFactor);
|
||||
|
||||
const rer = calculateRER(weightKg);
|
||||
const mer = calculateMER(rer, factor);
|
||||
|
||||
currentMER = mer;
|
||||
|
||||
document.getElementById('rerValue').textContent = formatNumber(rer, 0) + ' cal/day';
|
||||
document.getElementById('merValue').textContent = formatNumber(mer, 0) + ' cal/day';
|
||||
document.getElementById('calorieResults').style.display = 'block';
|
||||
|
||||
updateFoodCalculations();
|
||||
}
|
||||
|
||||
function updateFoodCalculations() {
|
||||
if (currentMER === 0) return;
|
||||
|
||||
const foodEnergy = document.getElementById('foodEnergy').value;
|
||||
const days = document.getElementById('days').value;
|
||||
const unit = document.getElementById('unit').value;
|
||||
|
||||
showError('foodEnergyError', false);
|
||||
showError('daysError', false);
|
||||
|
||||
if (!foodEnergy || !validateInput(foodEnergy, 1)) {
|
||||
if (foodEnergy) showError('foodEnergyError', true);
|
||||
document.getElementById('dailyFoodResults').style.display = 'none';
|
||||
document.getElementById('totalFoodDisplay').value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!days || !validateInput(days, 1, true)) {
|
||||
if (days) showError('daysError', true);
|
||||
document.getElementById('totalFoodDisplay').value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const energyPer100g = parseFloat(foodEnergy);
|
||||
const numDays = parseInt(days);
|
||||
|
||||
const dailyFoodGrams = (currentMER / energyPer100g) * 100;
|
||||
const totalFoodGrams = dailyFoodGrams * numDays;
|
||||
|
||||
document.getElementById('dailyFoodValue').textContent = formatNumber(dailyFoodGrams, 1) + ' g/day';
|
||||
document.getElementById('dailyFoodResults').style.display = 'block';
|
||||
|
||||
const convertedAmount = convertUnits(totalFoodGrams, unit);
|
||||
const unitLabel = unit === 'g' ? 'g' : unit === 'kg' ? 'kg' : unit === 'oz' ? 'oz' : 'lb';
|
||||
const decimals = unit === 'g' ? 0 : unit === 'kg' ? 2 : 1;
|
||||
|
||||
document.getElementById('totalFoodDisplay').value = formatNumber(convertedAmount, decimals) + ' ' + unitLabel;
|
||||
}
|
||||
|
||||
document.getElementById('weight').addEventListener('input', updateCalorieCalculations);
|
||||
document.getElementById('dogType').addEventListener('change', updateCalorieCalculations);
|
||||
document.getElementById('foodEnergy').addEventListener('input', updateFoodCalculations);
|
||||
document.getElementById('days').addEventListener('input', updateFoodCalculations);
|
||||
document.getElementById('unit').addEventListener('change', updateFoodCalculations);
|
||||
|
||||
document.getElementById('weight').addEventListener('blur', function() {
|
||||
const weight = this.value;
|
||||
if (weight && !validateInput(weight, 0.1)) {
|
||||
showError('weightError', true);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('foodEnergy').addEventListener('blur', function() {
|
||||
const energy = this.value;
|
||||
if (energy && !validateInput(energy, 1)) {
|
||||
showError('foodEnergyError', true);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('days').addEventListener('blur', function() {
|
||||
const days = this.value;
|
||||
if (days && !validateInput(days, 1, true)) {
|
||||
showError('daysError', true);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1729
dog-food-calculator-widget.js
Normal file
1729
dog-food-calculator-widget.js
Normal file
File diff suppressed because it is too large
Load Diff
1691
iframe.html
Normal file
1691
iframe.html
Normal file
File diff suppressed because it is too large
Load Diff
1460
sundog-dog-food-calculator.js
Normal file
1460
sundog-dog-food-calculator.js
Normal file
File diff suppressed because it is too large
Load Diff
37
test-widget.html
Normal file
37
test-widget.html
Normal file
@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Widget Test</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.test-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin: 20px 0;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Dog Calculator Widget Test</h1>
|
||||
|
||||
<div class="test-container">
|
||||
<h2>Test 1: Basic Widget</h2>
|
||||
<div id="dog-calorie-calculator"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-container">
|
||||
<h2>Test 2: Dark Theme Widget</h2>
|
||||
<div id="dog-calorie-calculator" data-theme="dark" data-scale="0.5"></div>
|
||||
</div>
|
||||
|
||||
<script src="sundog-dog-food-calculator.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user