diff --git a/package.json b/package.json index d2605742e..88b618718 100644 --- a/package.json +++ b/package.json @@ -86,9 +86,9 @@ }, "scripts": { "dev": "react-scripts start", - "start:nolint": "npx --yes create-react-play@latest -p && react-scripts start", - "start": "npx --yes create-react-play@latest -p && npm run lint && react-scripts start", - "build": "npx --yes create-react-play@latest -p && react-scripts build", + "start:nolint": "npx --yes create-react-play@latest -p && node scripts/sanitize-play-exports.cjs && react-scripts start", + "start": "npx --yes create-react-play@latest -p && node scripts/sanitize-play-exports.cjs && npm run lint && react-scripts start", + "build": "npx --yes create-react-play@latest -p && node scripts/sanitize-play-exports.cjs && react-scripts build", "snap": "react-snap", "test": "react-scripts test", "eject": "react-scripts eject", diff --git a/scripts/sanitize-play-exports.cjs b/scripts/sanitize-play-exports.cjs new file mode 100644 index 000000000..93c0f8b81 --- /dev/null +++ b/scripts/sanitize-play-exports.cjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const indexPath = path.join(process.cwd(), 'src', 'plays', 'index.js'); + +if (!fs.existsSync(indexPath)) { + console.warn(`[sanitize-play-exports] Skipped: file not found at ${indexPath}`); + process.exit(0); +} + +const source = fs.readFileSync(indexPath, 'utf8'); +const newline = source.includes('\r\n') ? '\r\n' : '\n'; +const hasTrailingNewline = source.endsWith('\n'); +const lines = source.split(/\r?\n/); + +const exportLinePattern = /^(\s*export\s*\{\s*default\s+as\s+)([^}]+?)(\s*\}\s*from\s*['"][^'"]+['"]\s*;?\s*)$/; +const isValidIdentifier = (value) => /^[$A-Z_a-z][$0-9A-Z_a-z]*$/.test(value); + +const toPascalCase = (value) => { + const chunks = value + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .split(/[^0-9A-Z_a-z$]+/) + .filter(Boolean); + + let identifier = chunks + .map((chunk) => chunk.charAt(0).toUpperCase() + chunk.slice(1)) + .join(''); + + if (identifier.length === 0) { + identifier = 'Play'; + } + + if (!/^[$A-Z_a-z]/.test(identifier)) { + identifier = `Play${identifier}`; + } + + return identifier; +}; + +const usedAliases = new Set(); +let updateCount = 0; + +const nextLines = lines.map((line) => { + const match = line.match(exportLinePattern); + if (match == null) { + return line; + } + + const [, prefix, rawAlias, suffix] = match; + const currentAlias = rawAlias.trim(); + let nextAlias = currentAlias; + + if (!isValidIdentifier(currentAlias)) { + nextAlias = toPascalCase(currentAlias); + } + + const aliasBase = nextAlias; + let duplicateIndex = 2; + while (usedAliases.has(nextAlias)) { + nextAlias = `${aliasBase}${duplicateIndex}`; + duplicateIndex += 1; + } + usedAliases.add(nextAlias); + + if (nextAlias !== currentAlias) { + updateCount += 1; + return `${prefix}${nextAlias}${suffix}`; + } + + return line; +}); + +let nextSource = nextLines.join(newline); +if (hasTrailingNewline) { + nextSource += newline; +} + +if (nextSource !== source) { + fs.writeFileSync(indexPath, nextSource, 'utf8'); + console.log(`[sanitize-play-exports] Updated ${updateCount} export alias(es).`); +} else { + console.log('[sanitize-play-exports] No invalid aliases found.'); +} diff --git a/src/plays/bmr-tdee-calculator/BmrTdeeCalculator.jsx b/src/plays/bmr-tdee-calculator/BmrTdeeCalculator.jsx new file mode 100644 index 000000000..31a4ae1fa --- /dev/null +++ b/src/plays/bmr-tdee-calculator/BmrTdeeCalculator.jsx @@ -0,0 +1,263 @@ +import PlayHeader from 'common/playlists/PlayHeader'; +import { useState } from 'react'; +import './styles.css'; + +// WARNING: Do not change the entry component name +function BmrTdeeCalculator(props) { + const [gender, setGender] = useState('male'); + const [age, setAge] = useState(''); + const [weight, setWeight] = useState(''); + const [height, setHeight] = useState(''); + const [activityLevel, setActivityLevel] = useState('1.2'); + const [unit, setUnit] = useState('metric'); + const [result, setResult] = useState(null); + + const activityOptions = [ + { value: '1.2', label: 'Sedentary', desc: 'Little or no exercise' }, + { value: '1.375', label: 'Lightly Active', desc: 'Exercise 1-3 days/week' }, + { value: '1.55', label: 'Moderately Active', desc: 'Exercise 3-5 days/week' }, + { value: '1.725', label: 'Very Active', desc: 'Exercise 6-7 days/week' }, + { value: '1.9', label: 'Extra Active', desc: 'Very hard exercise or physical job' } + ]; + + const calculateBMR = () => { + const ageNum = parseFloat(age); + let weightKg = parseFloat(weight); + let heightCm = parseFloat(height); + + if (!ageNum || !weightKg || !heightCm) { + return; + } + + // Convert imperial to metric if needed + if (unit === 'imperial') { + weightKg = weightKg * 0.453592; // lbs to kg + heightCm = heightCm * 2.54; // inches to cm + } + + // Mifflin-St Jeor Equation + let bmr; + if (gender === 'male') { + bmr = 10 * weightKg + 6.25 * heightCm - 5 * ageNum + 5; + } else { + bmr = 10 * weightKg + 6.25 * heightCm - 5 * ageNum - 161; + } + + const activity = parseFloat(activityLevel); + const tdee = bmr * activity; + + setResult({ + bmr: Math.round(bmr), + tdee: Math.round(tdee), + deficit: Math.round(tdee - 500), + surplus: Math.round(tdee + 500) + }); + }; + + const handleReset = () => { + setGender('male'); + setAge(''); + setWeight(''); + setHeight(''); + setActivityLevel('1.2'); + setUnit('metric'); + setResult(null); + }; + + return ( +
+ +
+
+

BMR & TDEE Calculator

+

+ Calculate your Basal Metabolic Rate and Total Daily Energy Expenditure +

+ +
+ {/* Unit Toggle */} +
+ +
+ + +
+
+ + {/* Gender */} +
+ +
+ + +
+
+ + {/* Age */} +
+ + setAge(e.target.value)} + /> +
+ + {/* Weight */} +
+ + setWeight(e.target.value)} + /> +
+ + {/* Height */} +
+ + setHeight(e.target.value)} + /> +
+ + {/* Activity Level */} +
+ + +
+ + {/* Buttons */} +
+ + +
+
+ + {/* Results */} + {result && ( +
+

Your Results

+
+
+ BMR + {result.bmr} + cal/day + Calories at complete rest +
+
+ TDEE + {result.tdee} + cal/day + Maintenance calories +
+
+ Weight Loss + {result.deficit} + cal/day + TDEE − 500 cal deficit +
+
+ Weight Gain + {result.surplus} + cal/day + TDEE + 500 cal surplus +
+
+ +
+

What do these numbers mean?

+
    +
  • + BMR — The calories your body burns at complete rest just to keep + you alive (breathing, circulation, cell production). +
  • +
  • + TDEE — Your total calories burned per day including physical + activity. Eat this amount to maintain weight. +
  • +
  • + Weight Loss — A 500 cal/day deficit leads to ~0.45 kg (1 lb) + lost per week. +
  • +
  • + Weight Gain — A 500 cal/day surplus leads to ~0.45 kg (1 lb) + gained per week. +
  • +
+

+ * Calculated using the Mifflin-St Jeor Equation, considered the most accurate BMR + formula. +

+
+
+ )} +
+
+
+ ); +} + +export default BmrTdeeCalculator; diff --git a/src/plays/bmr-tdee-calculator/Readme.md b/src/plays/bmr-tdee-calculator/Readme.md new file mode 100644 index 000000000..ed08e5d1c --- /dev/null +++ b/src/plays/bmr-tdee-calculator/Readme.md @@ -0,0 +1,41 @@ +# BMR & TDEE Calculator + +A calculator to determine your Basal Metabolic Rate (BMR) and Total Daily Energy Expenditure (TDEE) using the Mifflin-St Jeor Equation. + +## Play Demographic + +- Language: js +- Level: Beginner + +## Creator Information + +- User: aniketmishra-0 +- GitHub Link: https://github.com/aniketmishra-0 +- Blog: +- Video: + +## Implementation Details + +This play implements a BMR & TDEE calculator with the following features: + +- **BMR Calculation** using the Mifflin-St Jeor Equation (most accurate modern formula) +- **TDEE Calculation** based on 5 activity levels +- **Metric & Imperial** unit support (kg/cm and lbs/inches) +- **Weight management guidance** showing calorie targets for loss/gain +- Clean, responsive UI with smooth result animations + +### Formulas Used + +**Mifflin-St Jeor Equation:** +- Male: BMR = 10 × weight(kg) + 6.25 × height(cm) − 5 × age − 5 +- Female: BMR = 10 × weight(kg) + 6.25 × height(cm) − 5 × age − 161 + +**TDEE = BMR × Activity Multiplier** + +## Consideration + +- Results are estimates and should not replace professional medical advice. + +## Resources + +- [Mifflin-St Jeor Equation](https://en.wikipedia.org/wiki/Basal_metabolic_rate) diff --git a/src/plays/bmr-tdee-calculator/cover.svg b/src/plays/bmr-tdee-calculator/cover.svg new file mode 100644 index 000000000..956ed81f9 --- /dev/null +++ b/src/plays/bmr-tdee-calculator/cover.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + BMR & TDEE + Calculator + Basal Metabolic Rate • Total Daily Energy Expenditure + + 🔥 + + + diff --git a/src/plays/bmr-tdee-calculator/styles.css b/src/plays/bmr-tdee-calculator/styles.css new file mode 100644 index 000000000..c69985b28 --- /dev/null +++ b/src/plays/bmr-tdee-calculator/styles.css @@ -0,0 +1,292 @@ +.bmr-tdee-container { + max-width: 680px; + margin: 0 auto; + padding: 2rem 1.5rem; + font-family: var(--ff-default, 'Inter', sans-serif); +} + +.bmr-tdee-title { + font-size: 1.75rem; + font-weight: 700; + text-align: center; + margin-bottom: 0.25rem; + color: var(--color-neutral-90, #1a1a2e); +} + +.bmr-tdee-subtitle { + text-align: center; + color: var(--color-neutral-50, #6b7280); + font-size: 0.95rem; + margin-bottom: 2rem; +} + +/* Form */ +.bmr-tdee-form { + background: var(--color-neutral-10, #ffffff); + border: 1px solid var(--color-neutral-30, #e5e7eb); + border-radius: 1rem; + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.bmr-tdee-field { + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.bmr-tdee-label { + font-size: 0.85rem; + font-weight: 600; + color: var(--color-neutral-70, #374151); +} + +.bmr-tdee-input, +.bmr-tdee-select { + padding: 0.65rem 0.85rem; + border: 1px solid var(--color-neutral-30, #d1d5db); + border-radius: 0.5rem; + font-size: 0.95rem; + color: var(--color-neutral-90, #1f2937); + background: var(--color-neutral-10, #fff); + transition: border-color 0.2s ease, box-shadow 0.2s ease; + outline: none; +} + +.bmr-tdee-input:focus, +.bmr-tdee-select:focus { + border-color: var(--color-brand-primary, #00f2fe); + box-shadow: 0 0 0 3px rgba(0, 242, 254, 0.15); +} + +.bmr-tdee-input::placeholder { + color: var(--color-neutral-40, #9ca3af); +} + +/* Toggle group */ +.bmr-tdee-toggle-group { + display: flex; + gap: 0.5rem; +} + +.bmr-tdee-toggle-btn { + flex: 1; + padding: 0.55rem 0.75rem; + border: 1px solid var(--color-neutral-30, #d1d5db); + border-radius: 0.5rem; + background: var(--color-neutral-10, #fff); + color: var(--color-neutral-60, #6b7280); + font-size: 0.85rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.bmr-tdee-toggle-btn:hover { + border-color: var(--color-brand-primary, #00f2fe); +} + +.bmr-tdee-toggle-btn.active { + background: var(--color-brand-primary, #00f2fe); + border-color: var(--color-brand-primary, #00f2fe); + color: var(--color-neutral-90, #1a1a2e); + font-weight: 600; +} + +/* Buttons */ +.bmr-tdee-btn-group { + display: flex; + gap: 0.75rem; + margin-top: 0.5rem; +} + +.bmr-tdee-btn { + flex: 1; + padding: 0.7rem 1rem; + border: none; + border-radius: 0.5rem; + font-size: 0.95rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.bmr-tdee-btn-primary { + background: var(--color-brand-primary, #00f2fe); + color: var(--color-neutral-90, #1a1a2e); +} + +.bmr-tdee-btn-primary:hover { + opacity: 0.9; + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 242, 254, 0.3); +} + +.bmr-tdee-btn-secondary { + background: transparent; + border: 1px solid var(--color-neutral-30, #d1d5db); + color: var(--color-neutral-60, #6b7280); +} + +.bmr-tdee-btn-secondary:hover { + border-color: var(--color-neutral-50, #9ca3af); + color: var(--color-neutral-80, #374151); +} + +/* Results */ +.bmr-tdee-results { + margin-top: 2rem; + animation: bmrFadeIn 0.4s ease; +} + +@keyframes bmrFadeIn { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.bmr-tdee-results-title { + font-size: 1.25rem; + font-weight: 700; + text-align: center; + margin-bottom: 1.25rem; + color: var(--color-neutral-90, #1a1a2e); +} + +.bmr-tdee-results-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.bmr-tdee-result-card { + display: flex; + flex-direction: column; + align-items: center; + padding: 1.25rem 1rem; + border-radius: 0.75rem; + border: 1px solid var(--color-neutral-30, #e5e7eb); + background: var(--color-neutral-10, #fff); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.bmr-tdee-result-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.08); +} + +.bmr-tdee-card-bmr { + border-top: 3px solid #6366f1; +} + +.bmr-tdee-card-tdee { + border-top: 3px solid #00f2fe; +} + +.bmr-tdee-card-deficit { + border-top: 3px solid #f59e0b; +} + +.bmr-tdee-card-surplus { + border-top: 3px solid #10b981; +} + +.bmr-tdee-result-label { + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--color-neutral-50, #6b7280); + margin-bottom: 0.25rem; +} + +.bmr-tdee-result-value { + font-size: 2rem; + font-weight: 800; + color: var(--color-neutral-90, #1a1a2e); + line-height: 1.1; +} + +.bmr-tdee-result-unit { + font-size: 0.8rem; + color: var(--color-neutral-50, #9ca3af); + margin-bottom: 0.35rem; +} + +.bmr-tdee-result-desc { + font-size: 0.75rem; + color: var(--color-neutral-40, #9ca3af); + text-align: center; +} + +/* Info section */ +.bmr-tdee-info { + margin-top: 1.75rem; + background: var(--color-neutral-10, #f9fafb); + border: 1px solid var(--color-neutral-30, #e5e7eb); + border-radius: 0.75rem; + padding: 1.25rem 1.5rem; +} + +.bmr-tdee-info h4 { + font-size: 1rem; + font-weight: 700; + margin-bottom: 0.75rem; + color: var(--color-neutral-90, #1f2937); +} + +.bmr-tdee-info ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.bmr-tdee-info li { + font-size: 0.85rem; + color: var(--color-neutral-60, #4b5563); + line-height: 1.5; +} + +.bmr-tdee-info li strong { + color: var(--color-neutral-80, #1f2937); +} + +.bmr-tdee-info-note { + margin-top: 0.75rem; + font-size: 0.75rem; + color: var(--color-neutral-40, #9ca3af); + font-style: italic; +} + +/* Responsive */ +@media screen and (max-width: 520px) { + .bmr-tdee-container { + padding: 1rem; + } + + .bmr-tdee-title { + font-size: 1.4rem; + } + + .bmr-tdee-results-grid { + grid-template-columns: 1fr; + } + + .bmr-tdee-toggle-group { + flex-direction: column; + } + + .bmr-tdee-result-value { + font-size: 1.6rem; + } +}