Compare commits

...

25 Commits

Author SHA1 Message Date
Dayowe
69739c63a9 Update 2025-06-15 22:23:59 +02:00
Dayowe
fab1e5f400 Delete files 2025-06-15 22:23:26 +02:00
Dayowe
6af1f8fa13 Update file 2025-06-15 21:57:39 +02:00
Dayowe
e430783717 Initial commit 2025-06-15 21:57:27 +02:00
Dayowe
112951c240 Initial commit 2025-06-15 20:50:45 +02:00
Dayowe
28f5a459dd fixes 2025-06-09 10:12:28 +02:00
Dayowe
d3bec564a9 Scale option 2025-06-09 10:01:29 +02:00
Dayowe
768659a74f Fix border 2025-06-09 09:58:09 +02:00
Dayowe
bb04a4d63f Add North American calorie unit support (kcal/kg, kcal/cup, kcal/can) 2025-06-09 09:53:16 +02:00
Dayowe
46b3560a68 Update iframe .. rewrite a bit 2025-06-08 23:45:32 +02:00
Dayowe
c7cb3a2acb icons 2025-06-08 23:12:52 +02:00
Dayowe
4d29490f0c fix urls 2025-06-08 23:09:10 +02:00
Dayowe
f1de59e368 correct embed url 2025-06-08 23:04:59 +02:00
Dayowe
623149dc17 embed url 2025-06-08 23:02:57 +02:00
Dayowe
134ee583a2 Update url 2025-06-08 23:00:21 +02:00
Dayowe
23aa723b5f Update better, attempt visual hierarchy 2025-06-08 22:54:52 +02:00
Dayowe
6e28d3b273 Update better 2025-06-08 22:50:22 +02:00
Dayowe
aff9fb8721 Update 2025-06-08 22:46:43 +02:00
Dayowe
e84481ecb1 Update 2025-06-08 22:43:47 +02:00
Dayowe
45c243eb0b Update 2025-06-08 22:38:08 +02:00
Dayowe
954321fe6f Update 2025-06-08 22:27:21 +02:00
Dayowe
38ea3669cf Dark/light/system theme option 2025-06-08 22:00:13 +02:00
Dayowe
4eda60df2a Update 2025-06-08 21:45:16 +02:00
Dayowe
9fa91da90e Add index.html 2025-06-08 21:27:49 +02:00
Dayowe
5afb3a8c5a Update 2025-06-08 20:31:11 +02:00
7 changed files with 5724 additions and 437 deletions

265
README.md Normal file
View 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
View 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 };

View File

@ -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>

File diff suppressed because it is too large Load Diff

1691
iframe.html Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

37
test-widget.html Normal file
View 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>