495 lines
17 KiB
JavaScript
495 lines
17 KiB
JavaScript
|
|
/**
|
||
|
|
* Dog Calorie Calculator
|
||
|
|
* Calculates dog's daily calorie requirements and food amounts
|
||
|
|
* by Canine Nutrition and Wellness
|
||
|
|
*/
|
||
|
|
|
||
|
|
class DogCalorieCalculator {
|
||
|
|
constructor() {
|
||
|
|
this.currentMER = 0;
|
||
|
|
this.isImperial = false;
|
||
|
|
this.init();
|
||
|
|
}
|
||
|
|
|
||
|
|
init() {
|
||
|
|
this.bindEvents();
|
||
|
|
this.updateUnitLabels();
|
||
|
|
}
|
||
|
|
|
||
|
|
bindEvents() {
|
||
|
|
const weightInput = document.getElementById('weight');
|
||
|
|
const dogTypeSelect = document.getElementById('dogType');
|
||
|
|
const foodEnergyInput = document.getElementById('foodEnergy');
|
||
|
|
const daysInput = document.getElementById('days');
|
||
|
|
const unitSelect = document.getElementById('unit');
|
||
|
|
const unitToggle = document.getElementById('unitToggle');
|
||
|
|
|
||
|
|
if (weightInput) weightInput.addEventListener('input', () => this.updateCalorieCalculations());
|
||
|
|
if (weightInput) weightInput.addEventListener('blur', () => this.validateWeight());
|
||
|
|
|
||
|
|
if (dogTypeSelect) dogTypeSelect.addEventListener('change', () => this.updateCalorieCalculations());
|
||
|
|
|
||
|
|
if (foodEnergyInput) foodEnergyInput.addEventListener('input', () => this.updateFoodCalculations());
|
||
|
|
if (foodEnergyInput) foodEnergyInput.addEventListener('blur', () => this.validateFoodEnergy());
|
||
|
|
|
||
|
|
if (daysInput) daysInput.addEventListener('input', () => this.updateFoodCalculations());
|
||
|
|
if (daysInput) daysInput.addEventListener('blur', () => this.validateDays());
|
||
|
|
|
||
|
|
if (unitSelect) unitSelect.addEventListener('change', () => this.updateFoodCalculations());
|
||
|
|
|
||
|
|
if (unitToggle) unitToggle.addEventListener('change', () => this.toggleUnits());
|
||
|
|
}
|
||
|
|
|
||
|
|
calculateRER(weightKg) {
|
||
|
|
return 70 * Math.pow(weightKg, 0.75);
|
||
|
|
}
|
||
|
|
|
||
|
|
calculateMER(rer, factor) {
|
||
|
|
return rer * factor;
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
|
||
|
|
showError(elementId, show = true) {
|
||
|
|
const errorElement = document.getElementById(elementId);
|
||
|
|
if (errorElement) {
|
||
|
|
if (show) {
|
||
|
|
errorElement.classList.remove('dog-calculator-hidden');
|
||
|
|
} else {
|
||
|
|
errorElement.classList.add('dog-calculator-hidden');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
formatNumber(num, decimals = 0) {
|
||
|
|
return num.toFixed(decimals).replace(/\.?0+$/, '');
|
||
|
|
}
|
||
|
|
|
||
|
|
toggleUnits() {
|
||
|
|
const toggle = document.getElementById('unitToggle');
|
||
|
|
this.isImperial = toggle.checked;
|
||
|
|
|
||
|
|
this.updateUnitLabels();
|
||
|
|
this.convertExistingValues();
|
||
|
|
this.updateCalorieCalculations();
|
||
|
|
}
|
||
|
|
|
||
|
|
updateUnitLabels() {
|
||
|
|
const metricLabel = document.getElementById('metricLabel');
|
||
|
|
const imperialLabel = document.getElementById('imperialLabel');
|
||
|
|
const weightLabel = document.getElementById('weightLabel');
|
||
|
|
const foodEnergyLabel = document.getElementById('foodEnergyLabel');
|
||
|
|
const weightInput = document.getElementById('weight');
|
||
|
|
const foodEnergyInput = document.getElementById('foodEnergy');
|
||
|
|
const unitSelect = document.getElementById('unit');
|
||
|
|
|
||
|
|
if (metricLabel && imperialLabel) {
|
||
|
|
metricLabel.classList.toggle('active', !this.isImperial);
|
||
|
|
imperialLabel.classList.toggle('active', this.isImperial);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (this.isImperial) {
|
||
|
|
if (weightLabel) weightLabel.textContent = "Dog's Weight (lbs):";
|
||
|
|
if (weightInput) {
|
||
|
|
weightInput.placeholder = "Enter weight in lbs";
|
||
|
|
weightInput.min = "0.2"; // ~0.1kg in lbs
|
||
|
|
weightInput.step = "0.1";
|
||
|
|
}
|
||
|
|
if (foodEnergyLabel) foodEnergyLabel.textContent = "Food Energy Content (kcal/oz):";
|
||
|
|
if (foodEnergyInput) {
|
||
|
|
foodEnergyInput.placeholder = "Enter kcal per oz";
|
||
|
|
foodEnergyInput.min = "0.03"; // ~1 kcal/100g in kcal/oz
|
||
|
|
foodEnergyInput.step = "0.01";
|
||
|
|
}
|
||
|
|
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>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
} 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 (foodEnergyLabel) foodEnergyLabel.textContent = "Food Energy Content (kcal/100g):";
|
||
|
|
if (foodEnergyInput) {
|
||
|
|
foodEnergyInput.placeholder = "Enter kcal per 100g";
|
||
|
|
foodEnergyInput.min = "1";
|
||
|
|
foodEnergyInput.step = "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>
|
||
|
|
`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
convertExistingValues() {
|
||
|
|
const weightInput = document.getElementById('weight');
|
||
|
|
const foodEnergyInput = document.getElementById('foodEnergy');
|
||
|
|
|
||
|
|
if (weightInput && weightInput.value) {
|
||
|
|
const currentWeight = parseFloat(weightInput.value);
|
||
|
|
if (!isNaN(currentWeight)) {
|
||
|
|
if (this.isImperial) {
|
||
|
|
// Convert kg to lbs
|
||
|
|
weightInput.value = this.formatNumber(currentWeight * 2.20462, 1);
|
||
|
|
} else {
|
||
|
|
// Convert lbs to kg
|
||
|
|
weightInput.value = this.formatNumber(currentWeight / 2.20462, 1);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (foodEnergyInput && foodEnergyInput.value) {
|
||
|
|
const currentEnergy = parseFloat(foodEnergyInput.value);
|
||
|
|
if (!isNaN(currentEnergy)) {
|
||
|
|
if (this.isImperial) {
|
||
|
|
// Convert kcal/100g to kcal/oz
|
||
|
|
foodEnergyInput.value = this.formatNumber(currentEnergy / 3.52739, 2);
|
||
|
|
} else {
|
||
|
|
// Convert kcal/oz to kcal/100g
|
||
|
|
foodEnergyInput.value = this.formatNumber(currentEnergy * 3.52739, 0);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
getWeightInKg() {
|
||
|
|
const weightInput = document.getElementById('weight');
|
||
|
|
if (!weightInput || !weightInput.value) return null;
|
||
|
|
|
||
|
|
const weight = parseFloat(weightInput.value);
|
||
|
|
if (isNaN(weight)) return null;
|
||
|
|
|
||
|
|
return this.isImperial ? weight / 2.20462 : weight;
|
||
|
|
}
|
||
|
|
|
||
|
|
getFoodEnergyPer100g() {
|
||
|
|
const foodEnergyInput = document.getElementById('foodEnergy');
|
||
|
|
if (!foodEnergyInput || !foodEnergyInput.value) return null;
|
||
|
|
|
||
|
|
const energy = parseFloat(foodEnergyInput.value);
|
||
|
|
if (isNaN(energy)) return null;
|
||
|
|
|
||
|
|
return this.isImperial ? energy * 3.52739 : energy;
|
||
|
|
}
|
||
|
|
|
||
|
|
validateWeight() {
|
||
|
|
const weight = document.getElementById('weight')?.value;
|
||
|
|
if (weight && !this.validateInput(weight, 0.1)) {
|
||
|
|
this.showError('weightError', true);
|
||
|
|
} else {
|
||
|
|
this.showError('weightError', false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
validateFoodEnergy() {
|
||
|
|
const energy = document.getElementById('foodEnergy')?.value;
|
||
|
|
if (energy && !this.validateInput(energy, 1)) {
|
||
|
|
this.showError('foodEnergyError', true);
|
||
|
|
} else {
|
||
|
|
this.showError('foodEnergyError', false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
validateDays() {
|
||
|
|
const days = document.getElementById('days')?.value;
|
||
|
|
if (days && !this.validateInput(days, 1, true)) {
|
||
|
|
this.showError('daysError', true);
|
||
|
|
} else {
|
||
|
|
this.showError('daysError', false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
updateCalorieCalculations() {
|
||
|
|
const dogTypeSelect = document.getElementById('dogType');
|
||
|
|
const calorieResults = document.getElementById('calorieResults');
|
||
|
|
const rerValue = document.getElementById('rerValue');
|
||
|
|
const merValue = document.getElementById('merValue');
|
||
|
|
|
||
|
|
if (!dogTypeSelect || !calorieResults || !rerValue || !merValue) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const weightKg = this.getWeightInKg();
|
||
|
|
const dogTypeFactor = dogTypeSelect.value;
|
||
|
|
|
||
|
|
this.showError('weightError', false);
|
||
|
|
|
||
|
|
if (!weightKg || weightKg < 0.1) {
|
||
|
|
if (weightKg !== null) this.showError('weightError', true);
|
||
|
|
calorieResults.style.display = 'none';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!dogTypeFactor) {
|
||
|
|
calorieResults.style.display = 'none';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const factor = parseFloat(dogTypeFactor);
|
||
|
|
|
||
|
|
const rer = this.calculateRER(weightKg);
|
||
|
|
const mer = this.calculateMER(rer, factor);
|
||
|
|
|
||
|
|
this.currentMER = mer;
|
||
|
|
|
||
|
|
rerValue.textContent = this.formatNumber(rer, 0) + ' cal/day';
|
||
|
|
merValue.textContent = this.formatNumber(mer, 0) + ' cal/day';
|
||
|
|
calorieResults.style.display = 'block';
|
||
|
|
|
||
|
|
this.updateFoodCalculations();
|
||
|
|
}
|
||
|
|
|
||
|
|
updateFoodCalculations() {
|
||
|
|
if (this.currentMER === 0) return;
|
||
|
|
|
||
|
|
const daysInput = document.getElementById('days');
|
||
|
|
const unitSelect = document.getElementById('unit');
|
||
|
|
const dailyFoodResults = document.getElementById('dailyFoodResults');
|
||
|
|
const dailyFoodValue = document.getElementById('dailyFoodValue');
|
||
|
|
const totalFoodDisplay = document.getElementById('totalFoodDisplay');
|
||
|
|
|
||
|
|
if (!daysInput || !unitSelect || !dailyFoodResults || !dailyFoodValue || !totalFoodDisplay) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const energyPer100g = this.getFoodEnergyPer100g();
|
||
|
|
const days = daysInput.value;
|
||
|
|
const unit = unitSelect.value;
|
||
|
|
|
||
|
|
this.showError('foodEnergyError', false);
|
||
|
|
this.showError('daysError', false);
|
||
|
|
|
||
|
|
const minEnergy = this.isImperial ? 0.03 : 1;
|
||
|
|
if (!energyPer100g || energyPer100g < minEnergy) {
|
||
|
|
if (energyPer100g !== null) this.showError('foodEnergyError', true);
|
||
|
|
dailyFoodResults.style.display = 'none';
|
||
|
|
totalFoodDisplay.value = '';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!days || !this.validateInput(days, 1, true)) {
|
||
|
|
if (days) this.showError('daysError', true);
|
||
|
|
totalFoodDisplay.value = '';
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const numDays = parseInt(days);
|
||
|
|
|
||
|
|
const dailyFoodGrams = (this.currentMER / energyPer100g) * 100;
|
||
|
|
const totalFoodGrams = dailyFoodGrams * numDays;
|
||
|
|
|
||
|
|
dailyFoodValue.textContent = this.formatNumber(dailyFoodGrams, 1) + ' g/day';
|
||
|
|
dailyFoodResults.style.display = 'block';
|
||
|
|
|
||
|
|
const convertedAmount = this.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 = this.formatNumber(convertedAmount, decimals) + ' ' + unitLabel;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Global function for collapsible functionality (no longer used)
|
||
|
|
function toggleCollapsible(id) {
|
||
|
|
// Food section is now always expanded
|
||
|
|
}
|
||
|
|
|
||
|
|
// Initialize calculator when DOM is ready
|
||
|
|
document.addEventListener('DOMContentLoaded', function() {
|
||
|
|
new DogCalorieCalculator();
|
||
|
|
|
||
|
|
// Initialize share URL
|
||
|
|
const shareUrlInput = document.getElementById('shareUrl');
|
||
|
|
if (shareUrlInput) {
|
||
|
|
shareUrlInput.value = window.location.href;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Update embed codes with current domain
|
||
|
|
updateEmbedCodes();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Share and Embed Functions
|
||
|
|
function showShareOptions() {
|
||
|
|
const modal = document.getElementById('shareModal');
|
||
|
|
if (modal) {
|
||
|
|
modal.style.display = 'block';
|
||
|
|
document.body.style.overflow = 'hidden';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeShareModal() {
|
||
|
|
const modal = document.getElementById('shareModal');
|
||
|
|
if (modal) {
|
||
|
|
modal.style.display = 'none';
|
||
|
|
document.body.style.overflow = 'auto';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function showEmbedModal() {
|
||
|
|
const modal = document.getElementById('embedModal');
|
||
|
|
if (modal) {
|
||
|
|
modal.style.display = 'block';
|
||
|
|
document.body.style.overflow = 'hidden';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function closeEmbedModal() {
|
||
|
|
const modal = document.getElementById('embedModal');
|
||
|
|
if (modal) {
|
||
|
|
modal.style.display = 'none';
|
||
|
|
document.body.style.overflow = 'auto';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Share functions
|
||
|
|
function shareToFacebook() {
|
||
|
|
const url = encodeURIComponent(window.location.href);
|
||
|
|
const text = encodeURIComponent('Check out this Dog Calorie Calculator!');
|
||
|
|
window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}"e=${text}`, '_blank');
|
||
|
|
}
|
||
|
|
|
||
|
|
function shareToTwitter() {
|
||
|
|
const url = encodeURIComponent(window.location.href);
|
||
|
|
const text = encodeURIComponent('Calculate your dog\'s daily calorie needs with this helpful tool!');
|
||
|
|
window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank');
|
||
|
|
}
|
||
|
|
|
||
|
|
function shareToLinkedIn() {
|
||
|
|
const url = encodeURIComponent(window.location.href);
|
||
|
|
window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank');
|
||
|
|
}
|
||
|
|
|
||
|
|
function shareViaEmail() {
|
||
|
|
const subject = encodeURIComponent('Dog Calorie Calculator');
|
||
|
|
const body = encodeURIComponent(`Check out this helpful Dog Calorie Calculator:\n\n${window.location.href}\n\nIt helps calculate your dog's daily calorie needs based on their weight and activity level.`);
|
||
|
|
window.location.href = `mailto:?subject=${subject}&body=${body}`;
|
||
|
|
}
|
||
|
|
|
||
|
|
function copyShareLink() {
|
||
|
|
const shareUrlInput = document.getElementById('shareUrl');
|
||
|
|
if (shareUrlInput) {
|
||
|
|
shareUrlInput.select();
|
||
|
|
shareUrlInput.setSelectionRange(0, 99999); // For mobile devices
|
||
|
|
|
||
|
|
try {
|
||
|
|
document.execCommand('copy');
|
||
|
|
|
||
|
|
// Update button to show success
|
||
|
|
const copyBtn = document.querySelector('.dog-calculator-share-copy');
|
||
|
|
if (copyBtn) {
|
||
|
|
const originalHTML = copyBtn.innerHTML;
|
||
|
|
copyBtn.innerHTML = '<i class="fas fa-check"></i> Copied!';
|
||
|
|
copyBtn.style.background = '#7fa464';
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
copyBtn.innerHTML = originalHTML;
|
||
|
|
copyBtn.style.background = '';
|
||
|
|
}, 2000);
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to copy text: ', err);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Embed functions
|
||
|
|
function updateEmbedCodes() {
|
||
|
|
const currentDomain = window.location.origin;
|
||
|
|
const currentPath = window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/'));
|
||
|
|
const baseUrl = currentDomain + currentPath;
|
||
|
|
|
||
|
|
const widgetCode = document.getElementById('widgetCode');
|
||
|
|
const iframeCode = document.getElementById('iframeCode');
|
||
|
|
|
||
|
|
if (widgetCode) {
|
||
|
|
widgetCode.textContent = `<script src="${baseUrl}/dog-calculator-widget.js"></script>\n<div id="dog-calorie-calculator"></div>`;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (iframeCode) {
|
||
|
|
iframeCode.textContent = `<iframe src="${baseUrl}/iframe.html" \n width="100%" height="600" \n frameborder="0" \n title="Dog Calorie Calculator">\n</iframe>`;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function copyEmbedCode(type) {
|
||
|
|
const codeElement = type === 'widget' ? document.getElementById('widgetCode') : document.getElementById('iframeCode');
|
||
|
|
const button = event.target.closest('.dog-calculator-copy-btn');
|
||
|
|
|
||
|
|
if (codeElement && button) {
|
||
|
|
const textArea = document.createElement('textarea');
|
||
|
|
textArea.value = codeElement.textContent;
|
||
|
|
textArea.style.position = 'fixed';
|
||
|
|
textArea.style.left = '-999999px';
|
||
|
|
textArea.style.top = '-999999px';
|
||
|
|
document.body.appendChild(textArea);
|
||
|
|
textArea.focus();
|
||
|
|
textArea.select();
|
||
|
|
|
||
|
|
try {
|
||
|
|
document.execCommand('copy');
|
||
|
|
textArea.remove();
|
||
|
|
|
||
|
|
// Update button to show success
|
||
|
|
const originalHTML = button.innerHTML;
|
||
|
|
button.innerHTML = '<i class="fas fa-check"></i> Copied';
|
||
|
|
button.classList.add('copied');
|
||
|
|
|
||
|
|
setTimeout(() => {
|
||
|
|
button.innerHTML = originalHTML;
|
||
|
|
button.classList.remove('copied');
|
||
|
|
}, 2000);
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Failed to copy text: ', err);
|
||
|
|
textArea.remove();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Close modals when clicking outside
|
||
|
|
window.addEventListener('click', function(event) {
|
||
|
|
const shareModal = document.getElementById('shareModal');
|
||
|
|
const embedModal = document.getElementById('embedModal');
|
||
|
|
|
||
|
|
if (event.target === shareModal) {
|
||
|
|
closeShareModal();
|
||
|
|
} else if (event.target === embedModal) {
|
||
|
|
closeEmbedModal();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
// Close modals with Escape key
|
||
|
|
window.addEventListener('keydown', function(event) {
|
||
|
|
if (event.key === 'Escape') {
|
||
|
|
closeShareModal();
|
||
|
|
closeEmbedModal();
|
||
|
|
}
|
||
|
|
});
|