Raí Santos commited on
Commit
20c5151
·
1 Parent(s): 73f05d7
.gitignore ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dependencies
2
+ node_modules/
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+
7
+ # Runtime data
8
+ pids
9
+ *.pid
10
+ *.seed
11
+ *.pid.lock
12
+
13
+ # Coverage directory used by tools like istanbul
14
+ coverage/
15
+ *.lcov
16
+
17
+ # nyc test coverage
18
+ .nyc_output
19
+
20
+ # Grunt intermediate storage
21
+ .grunt
22
+
23
+ # Bower dependency directory
24
+ bower_components
25
+
26
+ # node-waf configuration
27
+ .lock-wscript
28
+
29
+ # Compiled binary addons
30
+ build/Release
31
+
32
+ # Dependency directories
33
+ jspm_packages/
34
+
35
+ # TypeScript v1 declaration files
36
+ typings/
37
+
38
+ # Optional npm cache directory
39
+ .npm
40
+
41
+ # Optional eslint cache
42
+ .eslintcache
43
+
44
+ # Optional REPL history
45
+ .node_repl_history
46
+
47
+ # Output of 'npm pack'
48
+ *.tgz
49
+
50
+ # Yarn Integrity file
51
+ .yarn-integrity
52
+
53
+ # dotenv environment variables file
54
+ .env
55
+ .env.test
56
+ .env.production
57
+ .env.local
58
+
59
+ # parcel-bundler cache
60
+ .cache
61
+ .parcel-cache
62
+
63
+ # next.js build output
64
+ .next
65
+
66
+ # nuxt.js build output
67
+ .nuxt
68
+
69
+ # vuepress build output
70
+ .vuepress/dist
71
+
72
+ # Serverless directories
73
+ .serverless
74
+
75
+ # FuseBox cache
76
+ .fusebox/
77
+
78
+ # DynamoDB Local files
79
+ .dynamodb/
80
+
81
+ # TernJS port file
82
+ .tern-port
83
+
84
+ # Stores VSCode versions used for testing VSCode extensions
85
+ .vscode-test
86
+
87
+ # Docker
88
+ .dockerignore
89
+
90
+ # OS generated files
91
+ .DS_Store
92
+ .DS_Store?
93
+ ._*
94
+ .Spotlight-V100
95
+ .Trashes
96
+ ehthumbs.db
97
+ Thumbs.db
98
+
99
+ # IDE files
100
+ .vscode/
101
+ .idea/
102
+ *.swp
103
+ *.swo
104
+ *~
105
+
106
+ # Logs
107
+ logs
108
+ *.log
Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use official Node.js runtime as base image
2
+ FROM node:18-alpine
3
+
4
+ # Set working directory in container
5
+ WORKDIR /app
6
+
7
+ # Copy package files
8
+ COPY package*.json ./
9
+
10
+ # Install dependencies
11
+ RUN npm ci --only=production && npm cache clean --force
12
+
13
+ # Copy application files
14
+ COPY . .
15
+
16
+ # Create non-root user for security
17
+ RUN addgroup -g 1001 -S nodejs && \
18
+ adduser -S nextjs -u 1001
19
+
20
+ # Change ownership of app directory
21
+ RUN chown -R nextjs:nodejs /app
22
+ USER nextjs
23
+
24
+ # Expose port
25
+ EXPOSE 7860
26
+
27
+ # Health check
28
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
29
+ CMD node -e "require('http').get('http://localhost:7860/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
30
+
31
+ # Start the application
32
+ CMD ["npm", "start"]
README.md CHANGED
@@ -1,11 +1,98 @@
1
  ---
2
- title: K30
3
- emoji: 🐠
4
  colorFrom: purple
5
- colorTo: yellow
6
  sdk: docker
 
7
  pinned: false
8
  license: mit
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Plano Cetogênico 30 Dias - Mapa da Secagem
3
+ emoji: 🔥
4
  colorFrom: purple
5
+ colorTo: pink
6
  sdk: docker
7
+ app_port: 7860
8
  pinned: false
9
  license: mit
10
  ---
11
 
12
+ # 🔥 Plano Cetogênico 30 Dias - Mapa da Secagem
13
+
14
+ Uma aplicação web completa e responsiva para acompanhar seu plano cetogênico de 30 dias, com **notificações programadas**, **PWA instalável** e orientações detalhadas.
15
+
16
+ ## ✨ Funcionalidades Principais
17
+
18
+ - **📱 PWA Instalável**: Instale como app no seu celular
19
+ - **🔔 Notificações Automáticas**: Lembretes em cada horário do plano:
20
+ - 05:00 - Despertar e hidratação
21
+ - 05:30 - Preparação para o dia
22
+ - 08:00 - Café da manhã cetogênico
23
+ - 12:00 - Almoço completo
24
+ - 13:00 - Suplementação
25
+ - 16:00 - Lanche pré-treino
26
+ - 17:00 - Treino turbo
27
+ - 18:00 - Jantar nutritivo
28
+ - 22:00 - Ceia e recuperação
29
+
30
+ - **✅ Checklist Completo**: Marque cada tarefa conforme completa
31
+ - **⏰ Cronômetro Integrado**: Para treinos e atividades
32
+ - **💪 Motivação Diária**: Versículos e mensagens encorajadoras
33
+ - **📊 Acompanhamento de Progresso**: Estatísticas detalhadas
34
+ - **🎨 Design Moderno**: Cores em tons de rosa escuro, roxo e azul escuro
35
+ - **📱 Mobile-First**: Interface otimizada para dispositivos móveis
36
+
37
+ ## 🚀 Como Usar
38
+
39
+ 1. **Acesse a aplicação** através do link do Hugging Face Spaces
40
+ 2. **Ative as notificações** clicando no botão "🔔 Ativar Notificações"
41
+ 3. **Instale como PWA** clicando em "📱 Instalar App" (quando disponível)
42
+ 4. **Navegue pelos dias** usando os controles de navegação
43
+ 5. **Marque as tarefas** conforme as completa
44
+ 6. **Use o cronômetro** para seus treinos
45
+
46
+ ## 📋 Plano Completo
47
+
48
+ ### 🍽️ Horários e Refeições
49
+ - **05:00** - Despertar: 300ml água + Lavitan Detox
50
+ - **08:00** - Café: 2 ovos + Lavitan Multi + C
51
+ - **12:00** - Almoço: Proteína + salada + Lavitan Energia
52
+ - **16:00** - Lanche: Shake Lavitan + Treonato
53
+ - **18:00** - Jantar: Ovos/frango + legumes + Lavitan Imunidade
54
+ - **22:00** - Ceia: Shake + Colágeno + Treonato
55
+
56
+ ### 🏃‍♀️ Treinos Diários
57
+ - **Segunda/Quinta**: Agachamento + Polichinelo + Prancha
58
+ - **Terça/Sexta**: Corrida parada + Abdominal + Polichinelo
59
+ - **Quarta**: Caminhada ou dança leve
60
+ - **Sábado**: Treino livre opcional
61
+ - **Domingo**: Descanso ativo
62
+
63
+ ### 🏆 Regras de Ouro
64
+ 1. **💧 Hidratação**: 2L de água por dia
65
+ 2. **🚫 Evitar**: Pão, macarrão, arroz, feijão, biscoito, refrigerante
66
+ 3. **🍽️ Preparação**: Cozinhe ovos e frango no domingo
67
+ 4. **😴 Descanso**: Durma até 00h no máximo
68
+
69
+ ## 💾 Tecnologias
70
+
71
+ - **Frontend**: HTML5, CSS3, JavaScript (Vanilla)
72
+ - **Backend**: Node.js + Express
73
+ - **PWA**: Service Worker + Web App Manifest
74
+ - **Notificações**: Web Notifications API
75
+ - **Containerização**: Docker
76
+ - **Deployment**: Hugging Face Spaces
77
+
78
+ ## 📈 Resultados Esperados
79
+
80
+ Seguindo o plano à risca, você pode esperar:
81
+ - **Perda de peso**: 6-9kg em 30 dias
82
+ - **Redução de medidas**: Especialmente na região abdominal
83
+ - **Mais energia**: Com a cetose e suplementação
84
+ - **Melhor disposição**: Com exercícios regulares
85
+
86
+ ## 🙏 Motivação
87
+
88
+ *"Posso todas as coisas naquele que me fortalece." - Filipenses 4:13*
89
+
90
+ Lembre-se: você é mais forte do que imagina! Cada dia é uma vitória, cada escolha é um passo em direção à sua melhor versão.
91
+
92
+ ## 🔒 Privacidade
93
+
94
+ Todos os seus dados ficam salvos localmente no seu dispositivo. Nenhuma informação pessoal é enviada para servidores externos.
95
+
96
+ ---
97
+
98
+ **Feito com 💜 para sua transformação!**
create-icons.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Simple script to create placeholder PNG icons from SVG
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ // Create icons directory if it doesn't exist
6
+ const iconsDir = path.join(__dirname, 'public', 'icons');
7
+ if (!fs.existsSync(iconsDir)) {
8
+ fs.mkdirSync(iconsDir, { recursive: true });
9
+ }
10
+
11
+ // Icon sizes needed for PWA
12
+ const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
13
+
14
+ // Create a simple PNG data URL for each size
15
+ sizes.forEach(size => {
16
+ // Create a simple colored square as placeholder
17
+ const canvas = `<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
18
+ <defs>
19
+ <linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
20
+ <stop offset="0%" style="stop-color:#1a0d2e;stop-opacity:1" />
21
+ <stop offset="50%" style="stop-color:#6b46c1;stop-opacity:1" />
22
+ <stop offset="100%" style="stop-color:#d946ef;stop-opacity:1" />
23
+ </linearGradient>
24
+ </defs>
25
+ <rect width="${size}" height="${size}" rx="${size * 0.1}" ry="${size * 0.1}" fill="url(#bg)"/>
26
+ <text x="${size/2}" y="${size/2}" font-family="Arial" font-size="${size * 0.3}" text-anchor="middle" dy="0.1em" fill="white">🔥</text>
27
+ </svg>`;
28
+
29
+ const filename = `icon-${size}x${size}.svg`;
30
+ fs.writeFileSync(path.join(iconsDir, filename), canvas);
31
+ console.log(`Created ${filename}`);
32
+ });
33
+
34
+ console.log('All icons created successfully!');
35
+ console.log('Note: For production, convert SVG icons to PNG using an online converter or image processing tool.');
dist/app.min.js ADDED
@@ -0,0 +1 @@
 
 
1
+ //Complete Fitness App-Redesigned class FitnessApp{constructor(){this.currentView='home';this.currentCategory=null;this.currentWorkout=null;this.workoutTimer=null;this.workoutStartTime=null;this.workoutSeconds=0;this.currentExerciseIndex=0;this.currentSeries=0;this.navigationHistory=[];this.userProfile=this.loadUserProfile();this.userData=this.loadUserData();this.progress=this.loadProgress();this.weightData=this.loadWeightData();this.personalPlan=null;this.soundEnabled=localStorage.getItem('soundEnabled')!=='false';this.audioContext=new(window.AudioContext||window.webkitAudioContext)();this.sounds={backgroundYoga: new Audio('songs/background_yoga.mp3'),startYoga: new Audio('songs/start_yoga.mp3'),countdown: new Audio('songs/td_countdown.mp3'),motivational: new Audio('songs/td_di_2.ogg')};this.setupAudio();this.init();}setupAudio(){this.sounds.backgroundYoga.loop=true;this.sounds.backgroundYoga.volume=0.3;this.audioLoaded=false;}ensureAudioLoaded(){if(this.audioLoaded)return;Object.values(this.sounds).forEach(sound=>{sound.load();});this.audioLoaded=true;}playCuteSound(type){if(!this.soundEnabled)return;try{ const ctx=this.audioContext; const oscillator=ctx.createOscillator(); const gainNode=ctx.createGain();oscillator.connect(gainNode);gainNode.connect(ctx.destination);switch(type){case 'tap':oscillator.frequency.setValueAtTime(800,ctx.currentTime);oscillator.frequency.exponentialRampToValueAtTime(400,ctx.currentTime+0.1);gainNode.gain.setValueAtTime(0.3,ctx.currentTime);gainNode.gain.exponentialRampToValueAtTime(0.01,ctx.currentTime+0.1);oscillator.type='sine';oscillator.start(ctx.currentTime);oscillator.stop(ctx.currentTime+0.1);break;case 'success':oscillator.frequency.setValueAtTime(523.25,ctx.currentTime);oscillator.frequency.setValueAtTime(659.25,ctx.currentTime+0.1);oscillator.frequency.setValueAtTime(783.99,ctx.currentTime+0.2);gainNode.gain.setValueAtTime(0.3,ctx.currentTime);gainNode.gain.exponentialRampToValueAtTime(0.01,ctx.currentTime+0.4);oscillator.type='sine';oscillator.start(ctx.currentTime);oscillator.stop(ctx.currentTime+0.4);break;case 'complete': const osc1=ctx.createOscillator(); const osc2=ctx.createOscillator(); const gain1=ctx.createGain(); const gain2=ctx.createGain();osc1.connect(gain1);osc2.connect(gain2);gain1.connect(ctx.destination);gain2.connect(ctx.destination);osc1.frequency.setValueAtTime(523.25,ctx.currentTime);osc2.frequency.setValueAtTime(659.25,ctx.currentTime);gain1.gain.setValueAtTime(0.2,ctx.currentTime);gain2.gain.setValueAtTime(0.2,ctx.currentTime);gain1.gain.exponentialRampToValueAtTime(0.01,ctx.currentTime+0.5);gain2.gain.exponentialRampToValueAtTime(0.01,ctx.currentTime+0.5);osc1.start(ctx.currentTime);osc2.start(ctx.currentTime);osc1.stop(ctx.currentTime+0.5);osc2.stop(ctx.currentTime+0.5);break;case 'click':oscillator.frequency.setValueAtTime(600,ctx.currentTime);gainNode.gain.setValueAtTime(0.2,ctx.currentTime);gainNode.gain.exponentialRampToValueAtTime(0.01,ctx.currentTime+0.05);oscillator.type='sine';oscillator.start(ctx.currentTime);oscillator.stop(ctx.currentTime+0.05);break;}}catch(e){console.log('Audio error:',e);}}playSound(soundName){if(!this.soundEnabled)return;this.ensureAudioLoaded(); const sound=this.sounds[soundName];if(sound){sound.currentTime=0;sound.play().catch(e=>{if(e.name!=='NotAllowedError'){console.log('Audio play failed:',e);}});}}stopSound(soundName){ const sound=this.sounds[soundName];if(sound){sound.pause();sound.currentTime=0;}}toggleSound(){this.soundEnabled=!this.soundEnabled;localStorage.setItem('soundEnabled',this.soundEnabled); const icon=document.getElementById('soundIcon');icon.textContent=this.soundEnabled?'🔊':'🔇';if(!this.soundEnabled){this.stopSound('backgroundYoga');}this.playCuteSound('click');}init(){if(!this.userProfile||!this.userProfile.name){this.showProfileSetup();return;} const isFirstTime=!localStorage.getItem('appVisited');if(isFirstTime){this.showWelcome();}else{this.hideWelcome();}this.generatePersonalPlan();this.setupEventListeners();this.resetDailyCalories();this.updateUI();this.loadAchievements();this.updateWeightDisplay();this.updateDetailedStats();this.setupPWA();}loadUserProfile(){try{ const data=localStorage.getItem('userProfile'); return data?JSON.parse(data):null;}catch(e){console.error('Error loading profile:',e); return null;}}saveUserProfile(){try{localStorage.setItem('userProfile',JSON.stringify(this.userProfile));}catch(e){console.error('Error saving profile:',e);}}showProfileSetup(){ const setupHTML=`<div class="profile-setup-screen" id="profileSetup"><div class="profile-setup-content"><h1 class="setup-title">🎯 Vamos Criar Seu Perfil</h1><p class="setup-subtitle">Para criar um plano personalizado perfeito para você!</p><form class="profile-form" id="profileForm"><!--Photo--><div class="form-group photo-upload"><label>📸 Foto de Perfil</label><div class="photo-selector"><input type="file" id="photoUpload" accept="image/*" style="display:none;"><div class="photo-preview" id="photoPreview"><div class="photo-placeholder"><span class="photo-icon">👤</span><span class="photo-text">Escolher Foto</span></div></div></div></div><!--Name--><div class="form-group"><label>✨ Nome</label><input type="text" id="profileName" placeholder="Como você se chama?" required></div><!--Age--><div class="form-row"><div class="form-group"><label>🎂 Idade</label><input type="number" id="profileAge" placeholder="25" min="13" max="100" required></div><!--Gender--><div class="form-group"><label>⚧ Sexo</label><select id="profileGender" required><option value="">Selecione</option><option value="female">Feminino</option><option value="male">Masculino</option><option value="other">Outro</option></select></div></div><!--Height&Weight--><div class="form-row"><div class="form-group"><label>📏 Altura(cm)</label><input type="number" id="profileHeight" placeholder="165" min="100" max="250" required></div><div class="form-group"><label>⚖️ Peso Atual(kg)</label><input type="number" id="profileWeight" placeholder="70" min="30" max="200" step="0.1" required></div></div><!--Goal Weight--><div class="form-group"><label>🎯 Peso Meta(kg)</label><input type="number" id="profileGoalWeight" placeholder="60" min="30" max="200" step="0.1" required></div><!--Activity Level--><div class="form-group"><label>🏃 Nível de Atividade</label><select id="profileActivity" required><option value="">Selecione</option><option value="sedentary">Sedentário(pouco ou nenhum exercício)</option><option value="light">Levemente Ativo(1-3 dias/semana)</option><option value="moderate">Moderadamente Ativo(3-5 dias/semana)</option><option value="active">Muito Ativo(6-7 dias/semana)</option><option value="extreme">Extremamente Ativo(atleta)</option></select></div><!--Goal--><div class="form-group"><label>💪 Objetivo Principal</label><select id="profileGoal" required><option value="">Selecione</option><option value="lose-weight">Perder Peso</option><option value="lose-weight-fast">Perder Peso Rápido</option><option value="maintain">Manter Peso</option><option value="gain-muscle">Ganhar Massa Muscular</option><option value="tone">Tonificar</option><option value="health">Melhorar Saúde Geral</option></select></div><!--Motivation--><div class="form-group"><label>🌟 Principal Motivação</label><select id="profileMotivation" required><option value="">Selecione</option><option value="health">Saúde e Bem-estar</option><option value="aesthetic">Estética/Aparência</option><option value="event">Evento Especial</option><option value="confidence">Autoconfiança</option><option value="energy">Mais Energia</option><option value="medical">Recomendação Médica</option></select></div><!--Preferences--><div class="form-group"><label>🍽️ Preferência Alimentar</label><select id="profileDiet"><option value="balanced">Balanceada</option><option value="low-carb">Low Carb</option><option value="keto">Cetogênica</option><option value="vegetarian">Vegetariana</option><option value="vegan">Vegana</option><option value="paleo">Paleo</option></select></div><button type="submit" class="btn-setup-submit">🚀 Criar Meu Plano Personalizado</button></form></div></div>`;document.body.insertAdjacentHTML('beforeend',setupHTML);this.setupProfileFormListeners();}setupProfileFormListeners(){ const form=document.getElementById('profileForm'); const photoUpload=document.getElementById('photoUpload'); const photoPreview=document.getElementById('photoPreview');photoPreview.addEventListener('click',()=>{photoUpload.click();});photoUpload.addEventListener('change',(e)=>{ const file=e.target.files[0];if(file){ const reader= new FileReader();reader.onload=(e)=>{ const img=document.createElement('img');img.src=e.target.result;img.className='profile-photo';photoPreview.innerHTML='';photoPreview.appendChild(img);this.tempPhoto=e.target.result;};reader.readAsDataURL(file);}});form.addEventListener('submit',(e)=>{e.preventDefault();this.saveProfile();});}saveProfile(){this.userProfile={photo:this.tempPhoto||null,name:document.getElementById('profileName').value,age:parseInt(document.getElementById('profileAge').value),gender:document.getElementById('profileGender').value,height:parseFloat(document.getElementById('profileHeight').value),weight:parseFloat(document.getElementById('profileWeight').value),goalWeight:parseFloat(document.getElementById('profileGoalWeight').value),activityLevel:document.getElementById('profileActivity').value,goal:document.getElementById('profileGoal').value,motivation:document.getElementById('profileMotivation').value,dietPreference:document.getElementById('profileDiet').value,createdAt: new Date().toISOString()};this.userProfile.bmi=this.calculateBMI(this.userProfile.weight,this.userProfile.height);this.userProfile.bmr=this.calculateBMR(this.userProfile.weight,this.userProfile.height,this.userProfile.age,this.userProfile.gender);this.userProfile.tdee=this.calculateTDEE(this.userProfile.bmr,this.userProfile.activityLevel);this.userProfile.targetCalories=this.calculateTargetCalories(this.userProfile.tdee,this.userProfile.goal);this.weightData.initial=this.userProfile.weight;this.weightData.current=this.userProfile.weight;this.weightData.goal=this.userProfile.goalWeight;this.weightData.history=[{date: new Date().toISOString().split('T')[0],weight:this.userProfile.weight}];this.saveWeightData();this.saveUserProfile();document.getElementById('profileSetup').remove();localStorage.setItem('appVisited','true');this.showWelcome();this.generatePersonalPlan();this.setupEventListeners();this.updateUI();this.loadAchievements();this.updateWeightDisplay();this.updateDetailedStats();this.setupPWA();}calculateBMI(weight,height){ const heightM=height/100;return(weight/(heightM*heightM)).toFixed(1);}calculateBMR(weight,height,age,gender){if(gender==='male'){return(10*weight)+(6.25*height)-(5*age)+5;}else{return(10*weight)+(6.25*height)-(5*age)-161;}}calculateTDEE(bmr,activityLevel){ const multipliers={sedentary:1.2,light:1.375,moderate:1.55,active:1.725,extreme:1.9}; return Math.round(bmr*(multipliers[activityLevel]||1.2));}calculateTargetCalories(tdee,goal){ const adjustments={'lose-weight':-500,'lose-weight-fast':-750,'maintain':0,'gain-muscle':300,'tone':-300,'health':-200}; return Math.round(tdee+(adjustments[goal]||-500));}generatePersonalPlan(){if(!this.userProfile)return; const profile=this.userProfile; const weightToLose=profile.weight-profile.goalWeight; const weeksToGoal=Math.ceil(Math.abs(weightToLose)/0.5);this.personalPlan={nutrition:{dailyCalories:profile.targetCalories,protein:Math.round(profile.weight*2),carbs:Math.round((profile.targetCalories*0.4)/4),fats:Math.round((profile.targetCalories*0.3)/9),water:Math.ceil(profile.weight*0.035),meals:this.generateMealPlan(profile)},workout:{frequency:this.getWorkoutFrequency(profile.activityLevel),duration:this.getWorkoutDuration(profile.goal),types:this.getRecommendedWorkouts(profile),intensity:this.getIntensityLevel(profile.activityLevel,profile.goal)},timeline:{weeksToGoal:weeksToGoal,expectedWeeklyLoss:0.5,milestones:this.generateMilestones(profile.weight,profile.goalWeight)},tips:this.generatePersonalizedTips(profile),coachMessages:this.generateCoachMessages(profile)};}getWorkoutFrequency(activityLevel){ const frequencies={sedentary:'3-4 vezes/semana',light:'4-5 vezes/semana',moderate:'5-6 vezes/semana',active:'6-7 vezes/semana',extreme:'7 vezes/semana+2 sessões duplas'}; return frequencies[activityLevel]||'3-4 vezes/semana';}getWorkoutDuration(goal){ const durations={'lose-weight':'45-60 minutos','lose-weight-fast':'60-90 minutos','maintain':'30-45 minutos','gain-muscle':'60-75 minutos','tone':'45-60 minutos','health':'30-45 minutos'}; return durations[goal]||'45-60 minutos';}getRecommendedWorkouts(profile){ const workouts=[];if(profile.goal.includes('lose-weight')){workouts.push('Cardio HIIT','Musculação','Caminhada Rápida');}if(profile.goal==='gain-muscle'){workouts.push('Musculação Pesada','Treino de Força');}if(profile.goal==='tone'){workouts.push('Musculação Moderada','Pilates','Yoga');} return workouts.length?workouts:['Treino Funcional','Cardio','Alongamento'];}getIntensityLevel(activity,goal){if(goal.includes('fast')) return 'Alta';if(activity==='sedentary') return 'Leve a Moderada';if(activity==='light') return 'Moderada'; return 'Moderada a Alta';}generateMealPlan(profile){return{breakfast:'Proteína+Carboidrato Complexo+Gordura Boa',snack1:'Fruta+Oleaginosas',lunch:'Proteína Magra+Vegetais+Carboidrato Integral',snack2:'Iogurte Natural ou Shake Proteico',dinner:'Proteína+Vegetais+Pouca Carbo',tips:['🥤 Beba água antes das refeições','🍽️ Use pratos menores','⏰ Coma devagar e mastigue bem','📵 Evite distrações durante refeições']};}generateMilestones(currentWeight,goalWeight){ const milestones=[]; const totalLoss=currentWeight-goalWeight; const steps=[0.25,0.5,0.75,1.0];steps.forEach(step=>{ const weight=currentWeight-(totalLoss*step);milestones.push({weight:weight.toFixed(1),percentage:(step*100).toFixed(0)});}); return milestones;}generatePersonalizedTips(profile){ const tips=[];if(profile.age<25){tips.push('💪 Seu metabolismo é acelerado!Aproveite para construir massa muscular.');} else if(profile.age>40){tips.push('🧘 Inclua mais treino de força para preser var massa muscular.');}if(profile.goal.includes('lose-weight')){tips.push('🔥 Combine cardio com musculação para queimar mais calorias.');tips.push('⏰ Faça jejum intermitente(16/8)se possível.');}if(profile.bmi>30){tips.push('🚶 Comece com caminhadas e aumente gradualmente.');}if(profile.dietPreference==='keto'){tips.push('🥑 Mantenha gorduras boas:abacate,oleaginosas,azeite.');} return tips;}generateCoachMessages(profile){ const messages=[]; const name=profile.name.split(' ')[0];messages.push(`Olá ${name}!Sou seu coach virtual e estou aqui para te ajudar!💪`);if(profile.motivation==='health'){messages.push('Saúde é riqueza!Vamos priorizar seu bem-estar. 🌟');} const weightToLose=profile.weight-profile.goalWeight;if(weightToLose>0){messages.push(`Você quer perder ${weightToLose.toFixed(1)}kg. Vamos conseguir juntos!🎯`);}messages.push('Lembre-se:consistência é mais importante que perfeição!💫'); return messages;}updatePlanSummary(){if(!this.personalPlan||!this.userProfile)return; const planSummary=document.getElementById('planSummary');if(!planSummary)return;planSummary.style.display='block'; const goalNames={'lose-weight':'Perder Peso','lose-weight-fast':'Perder Peso Rápido','maintain':'Manter Peso','gain-muscle':'Ganhar Massa','tone':'Tonificar','health':'Saúde'};document.getElementById('planGoal').textContent=goalNames[this.userProfile.goal]||this.userProfile.goal;document.getElementById('planCalories').textContent=`${this.personalPlan.nutrition.dailyCalories}kcal`;document.getElementById('planWorkout').textContent=this.personalPlan.workout.frequency.split(' ')[0]; const messages=this.personalPlan.coachMessages; const randomMessage=messages[Math.floor(Math.random()*messages.length)];document.getElementById('coachMessage').textContent=randomMessage;}showFullPlan(){if(!this.personalPlan||!this.userProfile)return; const profile=this.userProfile; const plan=this.personalPlan; const profileInfoHTML=`<div class="profile-photo-container">${profile.photo?`<img src="${profile.photo}" class="profile-photo-large" alt="Foto de perfil">`:'<div class="profile-photo-large" style="background:var(--gradient-primary);display:flex;align-items:center;justify-content:center;color:white;font-size:2rem;">👤</div>'}<div class="profile-basic-info"><div class="profile-name">${profile.name}</div><div style="color:var(--text-secondary);">${profile.age}anos • ${profile.gender==='female'?'Feminino':profile.gender==='male'?'Masculino':'Outro'}</div></div></div><div class="profile-metrics"><div class="metric-item"><span class="metric-label">Altura</span><span class="metric-value">${profile.height}cm</span></div><div class="metric-item"><span class="metric-label">Peso Atual</span><span class="metric-value">${profile.weight}kg</span></div><div class="metric-item"><span class="metric-label">Peso Meta</span><span class="metric-value">${profile.goalWeight}kg</span></div><div class="metric-item"><span class="metric-label">IMC</span><span class="metric-value">${profile.bmi}</span></div><div class="metric-item"><span class="metric-label">TMB</span><span class="metric-value">${Math.round(profile.bmr)}kcal</span></div><div class="metric-item"><span class="metric-label">TDEE</span><span class="metric-value">${profile.tdee}kcal</span></div></div>`; const nutritionHTML=`<div class="nutrition-grid"><div class="nutrition-item"><span class="nutrition-label">Calorias Diárias</span><span class="nutrition-value">${plan.nutrition.dailyCalories}<span class="nutrition-unit">kcal</span></span></div><div class="nutrition-item"><span class="nutrition-label">Proteína</span><span class="nutrition-value">${plan.nutrition.protein}<span class="nutrition-unit">g</span></span></div><div class="nutrition-item"><span class="nutrition-label">Carboidratos</span><span class="nutrition-value">${plan.nutrition.carbs}<span class="nutrition-unit">g</span></span></div><div class="nutrition-item"><span class="nutrition-label">Gorduras</span><span class="nutrition-value">${plan.nutrition.fats}<span class="nutrition-unit">g</span></span></div><div class="nutrition-item"><span class="nutrition-label">Água</span><span class="nutrition-value">${plan.nutrition.water}<span class="nutrition-unit">L</span></span></div><div class="nutrition-item"><span class="nutrition-label">Dieta</span><span class="nutrition-value" style="font-size:0.9rem;">${profile.dietPreference}</span></div></div><div class="meal-plan"><h4 style="margin-bottom:var(--spacing-sm);color:var(--text-primary);">📅 Estrutura de Refeições</h4><div class="meal-item"><div class="meal-name">🌅 Café da Manhã</div><div class="meal-description">${plan.nutrition.meals.breakfast}</div></div><div class="meal-item"><div class="meal-name">🍎 Lanche da Manhã</div><div class="meal-description">${plan.nutrition.meals.snack1}</div></div><div class="meal-item"><div class="meal-name">🍽️ Almoço</div><div class="meal-description">${plan.nutrition.meals.lunch}</div></div><div class="meal-item"><div class="meal-name">🥤 Lanche da Tarde</div><div class="meal-description">${plan.nutrition.meals.snack2}</div></div><div class="meal-item"><div class="meal-name">🌙 Jantar</div><div class="meal-description">${plan.nutrition.meals.dinner}</div></div></div><div style="margin-top:var(--spacing-md);"><h4 style="margin-bottom:var(--spacing-sm);color:var(--text-primary);">💡 Dicas Nutricionais</h4>${plan.nutrition.meals.tips.map(tip=>`<div style="padding:8px;color:var(--text-secondary);">${tip}</div>`).join('')}</div>`; const workoutHTML=`<div class="workout-info"><div class="info-row"><span class="info-label">Frequência</span><span class="info-value">${plan.workout.frequency}</span></div><div class="info-row"><span class="info-label">Duração</span><span class="info-value">${plan.workout.duration}</span></div><div class="info-row"><span class="info-label">Intensidade</span><span class="info-value">${plan.workout.intensity}</span></div><div class="info-row"><span class="info-label">Tipos Recomendados</span><span class="info-value">${plan.workout.types.join(',')}</span></div></div>`; const timelineHTML=`<div class="timeline-info"><div class="info-row"><span class="info-label">Tempo até a Meta</span><span class="info-value">${plan.timeline.weeksToGoal}semanas</span></div><div class="info-row"><span class="info-label">Perda Semanal Esperada</span><span class="info-value">${plan.timeline.expectedWeeklyLoss}kg/semana</span></div></div><div class="milestones"><h4 style="margin-top:var(--spacing-md);margin-bottom:var(--spacing-sm);color:var(--text-primary);">🎯 Marcos do Progresso</h4>${plan.timeline.milestones.map((milestone,index)=>`<div class="milestone-item"><div class="milestone-check">${index===0?'🎯':'💪'}</div><div style="flex:1;"><div style="font-weight:600;color:var(--text-primary);">${milestone.percentage}%da Meta</div><div style="font-size:0.9rem;color:var(--text-secondary);">${milestone.weight}kg</div></div></div>`).join('')}</div>`; const tipsHTML=`<div class="tips-list">${plan.tips.map(tip=>`<div class="tip-item">${tip}</div>`).join('')}</div>`;document.getElementById('profileInfo').innerHTML=profileInfoHTML;document.getElementById('nutritionPlan').innerHTML=nutritionHTML;document.getElementById('workoutPlan').innerHTML=workoutHTML;document.getElementById('timelinePlan').innerHTML=timelineHTML;document.getElementById('tipsPlan').innerHTML=tipsHTML;this.openModal('planModal');}editProfile(){localStorage.removeItem('userProfile');localStorage.removeItem('appVisited');this.userProfile=null;this.personalPlan=null;location.reload();}setupPWA(){window.addEventListener('beforeinstallprompt',(e)=>{e.preventDefault();this.deferredPrompt=e;console.log('PWA install available');});window.addEventListener('appinstalled',()=>{console.log('PWA installed successfully!');this.deferredPrompt=null;});}showWelcome(){document.getElementById('welcomeScreen').style.display='flex';document.getElementById('startJourney').addEventListener('click',()=>{localStorage.setItem('appVisited','true');this.hideWelcome();});}hideWelcome(){ const welcome=document.getElementById('welcomeScreen'); const app=document.getElementById('appContainer');welcome.style.animation='fadeOut 0.5s ease';setTimeout(()=>{welcome.style.display='none';app.style.display='block';},500);}setupEventListeners(){document.body.addEventListener('click',(e)=>{if(e.target.id==='viewFullPlan'||e.target.closest('#viewFullPlan')){e.preventDefault();this.showFullPlan();return;}if(e.target.id==='editProfile'||e.target.closest('#editProfile')){e.preventDefault();this.closeModal('planModal');this.editProfile();return;} const target=e.target.closest('[data-nav],[data-navigate],[data-back],[data-category],[data-wellness]');if(!target)return;if(target.dataset.nav){e.preventDefault();this.navigateTo(target.dataset.nav);}if(target.dataset.navigate){e.preventDefault();this.navigateTo(target.dataset.navigate);}if(target.dataset.back){e.preventDefault();this.goBack();}if(target.dataset.category){e.preventDefault();this.showCategoryExercises(target.dataset.category);}if(target.dataset.wellness){e.preventDefault();this.startWellnessSession(target.dataset.wellness);}}); const waterContainer=document.querySelector('.water-glasses');if(waterContainer){waterContainer.addEventListener('click',(e)=>{ const glass=e.target.closest('.glass');if(glass){e.stopPropagation(); const glasses=Array.from(waterContainer.querySelectorAll('.glass')); const index=glasses.indexOf(glass);if(index!==-1){this.toggleWater(index);}}});}document.body.addEventListener('click',(e)=>{if(e.target.id==='closeWorkout'||e.target.closest('#closeWorkout')){e.preventDefault();e.stopPropagation();this.endWorkout(false);}if(e.target.id==='completeExercise'||e.target.closest('#completeExercise')){e.preventDefault();e.stopPropagation();if(!e.target.disabled&&!e.target.closest('button')?.disabled){this.completeCurrentExercise();}}if(e.target.id==='skipExercise'||e.target.closest('#skipExercise')){e.preventDefault();e.stopPropagation();this.skipExercise();}if(e.target.id==='closeCompletionModal'||e.target.closest('#closeCompletionModal')){e.preventDefault();e.stopPropagation();document.getElementById('completionModal').classList.remove('active');this.playCuteSound('tap');if(this.currentCategory){this.goBack();}else{this.navigateTo('home');}}if(e.target.id==='notifBtn'||e.target.closest('#notifBtn')){e.preventDefault();e.stopPropagation();this.requestNotificationPermission();}if(e.target.id==='toggleSound'||e.target.closest('#toggleSound')){e.preventDefault();e.stopPropagation();this.toggleSound();}if(e.target.id==='updateWeightBtn'||e.target.closest('#updateWeightBtn')){e.preventDefault();e.stopPropagation();this.showWeightModal();}if(e.target.id==='saveWeightBtn'||e.target.closest('#saveWeightBtn')){e.preventDefault();e.stopPropagation();this.saveWeight();}if(e.target.id==='cancelWeightBtn'||e.target.closest('#cancelWeightBtn')){e.preventDefault();e.stopPropagation();this.closeWeightModal();}});document.addEventListener('click',(e)=>{ const clickableElements=['button','.glass','.nav-item','.category-card','.wellness-card','.action-card','.exercise-card'];if(clickableElements.some(selector=>e.target.closest(selector))){this.playCuteSound('click');}},true);}navigateTo(view,addToHistory=true){if(addToHistory&&this.currentView&&this.currentView!==view){this.navigationHistory.push(this.currentView);}document.querySelectorAll('.view').forEach(v=>v.classList.remove('active')); const targetView=document.getElementById(`${view}View`);if(targetView){targetView.classList.add('active');}document.querySelectorAll('.nav-item').forEach(item=>{item.classList.remove('active');if(item.dataset.nav===view){item.classList.add('active');}});this.currentView=view;}goBack(){if(this.navigationHistory.length>0){ const previousView=this.navigationHistory.pop();this.navigateTo(previousView,false);}else{this.navigateTo('home',false);}}showCategoryExercises(category){this.currentCategory=category; const exercises=this.getExercisesByCategory(category); const categoryNames={personalized:'Treino Personalizado',abs:'Abdômen',legs:'Pernas',glutes:'Glúteos',arms:'Braços',waist:'Cintura',back:'Costas',cardio:'Cardio',fullbody:'Corpo Todo',yoga:'Yoga',massage:'Massagem',face:'Rosto'};document.getElementById('categoryTitle').textContent=categoryNames[category]; const container=document.getElementById('exercisesList');container.innerHTML='';if(category==='personalized'){ const sections={'Abdômen':exercises.slice(0,5),'Rosto':exercises.slice(5,9),'Cintura':exercises.slice(9,12),'Pernas':exercises.slice(12,13),'Costas/Flexibilidade':exercises.slice(13,14)};Object.entries(sections).forEach(([sectionName,sectionExercises])=>{if(sectionExercises.length>0){ const sectionHeader=document.createElement('div');sectionHeader.className='section-header';sectionHeader.innerHTML=`<h3>${sectionName}</h3>`;container.appendChild(sectionHeader);sectionExercises.forEach((exercise,index)=>{ const globalIndex=exercises.indexOf(exercise); const card=document.createElement('div');card.className='exercise-card';card.innerHTML=`<div class="exercise-emoji">${exercise.emoji}</div><div class="exercise-info"><div class="exercise-name">${exercise.name}</div><div class="exercise-details">${exercise.sets}× ${exercise.reps}</div></div><div class="exercise-arrow">→</div>`;card.addEventListener('click',()=>{this.startWorkout(exercises,globalIndex);});container.appendChild(card);});}});}else{exercises.forEach((exercise,index)=>{ const card=document.createElement('div');card.className='exercise-card';card.innerHTML=`<div class="exercise-emoji">${exercise.emoji}</div><div class="exercise-info"><div class="exercise-name">${exercise.name}</div><div class="exercise-details">${exercise.sets}× ${exercise.reps}</div></div><div class="exercise-arrow">→</div>`;card.addEventListener('click',()=>{this.startWorkout(exercises,index);});container.appendChild(card);});}this.navigateTo('exercisesList');}startWorkout(exercises,startIndex=0){this.currentWorkout=exercises;this.currentExerciseIndex=startIndex;this.currentSeries=0;this.workoutStartTime=Date.now();this.playSound('startYoga');setTimeout(()=>{this.playSound('backgroundYoga');},2000);this.navigateTo('workoutSession');this.displayCurrentExercise();this.startWorkoutTimer();}displayCurrentExercise(){ const exercise=this.currentWorkout[this.currentExerciseIndex];document.getElementById('currentExerciseName').textContent=exercise.name;document.getElementById('exerciseCount').textContent=`Exercício ${this.currentExerciseIndex+1}de ${this.currentWorkout.length}`; const demoVideo=document.getElementById('demoVideo'); const demoIcon=document.getElementById('demoIcon');if(exercise.video){demoVideo.querySelector('source').src=exercise.video+'#t=4';demoVideo.load();demoVideo.play().catch(e=>console.log('Video autoplay prevented:',e));demoVideo.style.display='block';demoIcon.style.display='none';}else{demoVideo.style.display='none';demoIcon.style.display='block';demoIcon.textContent=exercise.emoji||'💪';}document.getElementById('repsInfo').textContent=`${exercise.sets}séries × ${exercise.reps}`;document.getElementById('restInfo').textContent=`Descanso:${exercise.rest||30}s entre séries`;this.updateSeriesTracker(exercise.sets); const progress=((this.currentExerciseIndex+(this.currentSeries/exercise.sets))/this.currentWorkout.length)*100;document.getElementById('workoutProgressBar').style.width=`${progress}%`;this.preloadNextVideo();}preloadNextVideo(){ const nextIndex=this.currentExerciseIndex+1;if(nextIndex<this.currentWorkout.length){ const nextExercise=this.currentWorkout[nextIndex];if(nextExercise.video){ const link=document.createElement('link');link.rel='preload';link.as='video';link.href=nextExercise.video;document.head.appendChild(link);}}}updateSeriesTracker(totalSeries){ const tracker=document.getElementById('seriesTracker');tracker.innerHTML='';for( let i=0;i<totalSeries;i++){ const dot=document.createElement('div');dot.className='series-dot';if(i<this.currentSeries){dot.classList.add('completed');}tracker.appendChild(dot);}}startWorkoutTimer(){this.workoutSeconds=0;this.workoutTimer=setInterval(()=>{this.workoutSeconds++; const mins=Math.floor(this.workoutSeconds/60); const secs=this.workoutSeconds%60;document.getElementById('workoutTimer').textContent=`${String(mins).padStart(2,'0')}:${String(secs).padStart(2,'0')}`;},1000);}completeCurrentExercise(){ const exercise=this.currentWorkout[this.currentExerciseIndex];this.currentSeries++;this.playCuteSound('success');if(this.currentSeries>=exercise.sets){this.currentExerciseIndex++;this.currentSeries=0;if(this.currentExerciseIndex>=this.currentWorkout.length){this.completeWorkout();}else{this.displayCurrentExercise();this.playCuteSound('tap');}}else{this.updateSeriesTracker(exercise.sets);this.showRestPeriod(exercise.rest||30);}}showRestPeriod(seconds){ const btn=document.getElementById('completeExercise'); const originalText=btn.textContent; let remaining=seconds;btn.textContent=`Descansando... ${remaining}s`;btn.disabled=true;if(remaining>5){this.playCuteSound('tap');} const restInterval=setInterval(()=>{remaining--;btn.textContent=`Descansando... ${remaining}s`;if(remaining<=3&&remaining>0){this.playSound('countdown');}if(remaining<=0){clearInterval(restInterval);btn.textContent=originalText;btn.disabled=false;this.playCuteSound('success');}},1000);}skipExercise(){this.currentExerciseIndex++;this.currentSeries=0;if(this.currentExerciseIndex>=this.currentWorkout.length){this.completeWorkout();}else{this.displayCurrentExercise();}}completeWorkout(){clearInterval(this.workoutTimer); const durationInSeconds=this.workoutSeconds||Math.floor((Date.now()-this.workoutStartTime)/1000); const duration=Math.max(1,Math.floor(durationInSeconds/60)); const calories=this.calculateCalories(duration,this.currentWorkout);console.log('Workout completed:',{durationInSeconds,duration,calories,exercises:this.currentWorkout});this.progress.workoutsCompleted=(this.progress.workoutsCompleted||0)+1;this.progress.totalMinutes=(this.progress.totalMinutes||0)+duration;this.progress.totalCalories=(this.progress.totalCalories||0)+calories;this.progress.lastWorkout= new Date().toISOString();if(!this.progress.longestWorkout||duration>this.progress.longestWorkout){this.progress.longestWorkout=duration;}if(!this.progress.workoutHistory){this.progress.workoutHistory=[];}this.progress.workoutHistory.push({date: new Date().toISOString(),duration:duration,calories:calories,category:this.currentCategory});this.updateStreak();this.saveProgress();this.stopSound('backgroundYoga');this.playCuteSound('complete');this.playSound('motivational');document.getElementById('summaryTime').textContent=`${duration}min`;document.getElementById('summaryCalories').textContent=`${calories}kcal`;document.getElementById('summaryExercises').textContent=`${this.currentWorkout.length}exercícios`; let detailsHTML='<strong>📊 Detalhes do Treino:</strong><br><br>';this.currentWorkout.forEach((exercise,index)=>{ const exerciseCalories=Math.round(this.getExerciseCaloriesPerMinute(exercise.name)*(duration/this.currentWorkout.length)*(exercise.sets/3));detailsHTML+=`<div style="margin-bottom:8px;"><strong>${index+1}. ${exercise.name}</strong><br><span style="font-size:0.85rem;">${exercise.sets}séries × ${exercise.reps}repetições<br>~${exerciseCalories}kcal queimadas</span></div>`;});document.getElementById('workoutDetails').innerHTML=detailsHTML;document.getElementById('completionModal').classList.add('active');this.checkAchievements();this.updateWeightBasedOnCalories();this.updateUI();this.updateDetailedStats();}endWorkout(completed=false){clearInterval(this.workoutTimer);this.workoutSeconds=0;this.stopSound('backgroundYoga');this.stopSound('startYoga');if(this.currentCategory){this.goBack();}else{this.navigateTo('home');}}getExerciseCaloriesPerMinute(exerciseName){ const name=exerciseName.toLowerCase(); let caloriesPerMinute=8;if(name.includes('scissor')||name.includes('tesoura')){caloriesPerMinute=12;} else if(name.includes('prancha')||name.includes('plank')){caloriesPerMinute=10;} else if(name.includes('elevação')||name.includes('leg lift')){caloriesPerMinute=9;} else if(name.includes('face')||name.includes('rosto')||name.includes('papada')||name.includes('chin')||name.includes('linha')||name.includes('esculpida')||name.includes('afinada')){caloriesPerMinute=3;} else if(name.includes('agachamento')||name.includes('squat')||name.includes('rotação')){caloriesPerMinute=11;} else if(name.includes('flexão')||name.includes('push')||name.includes('extensão')){caloriesPerMinute=10;} else if(name.includes('ponte')||name.includes('bridge')){caloriesPerMinute=9;} else if(name.includes('burpee')||name.includes('jump')){caloriesPerMinute=14;} else if(name.includes('polichinelo')||name.includes('jack')){caloriesPerMinute=13;} else if(name.includes('alongamento')||name.includes('stretch')){caloriesPerMinute=6;} else if(name.includes('torção')||name.includes('twist')){caloriesPerMinute=7;} else if(name.includes('yoga')){caloriesPerMinute=5;} else if(name.includes('massagem')||name.includes('massage')){caloriesPerMinute=2;} return caloriesPerMinute;}calculateCalories(minutes,workoutExercises=null){if(!workoutExercises||workoutExercises.length===0){ return Math.round(minutes*8);} let totalCalories=0; const minutesPerExercise=minutes/workoutExercises.length;workoutExercises.forEach(exercise=>{ const caloriesPerMinute=this.getExerciseCaloriesPerMinute(exercise.name); const sets=exercise.sets||3; const caloriesForThisExercise=caloriesPerMinute*minutesPerExercise*(sets/3);totalCalories+=caloriesForThisExercise;}); return Math.round(totalCalories);}updateWeightBasedOnCalories(){if(!this.userProfile||!this.weightData||this.weightData.length===0)return; const today= new Date().toDateString(); const lastWeightEntry=this.weightData[this.weightData.length-1]; const lastWeightDate= new Date(lastWeightEntry.date).toDateString();if(lastWeightDate===today)return; const caloriesIn=this.progress.dailyCaloriesConsumed||this.userProfile.targetCalories; const caloriesBurned=this.progress.totalCalories||0; const tdee=this.userProfile.tdee||2000; const dailyDeficit=(tdee+caloriesBurned)-caloriesIn; const weightChange=dailyDeficit/7700; const safeWeightChange=Math.max(-0.15,Math.min(0.15,weightChange)); const currentWeight=lastWeightEntry.weight; const newWeight=Math.max(30,currentWeight-safeWeightChange);this.weightData.push({date: new Date().toISOString(),weight:parseFloat(newWeight.toFixed(1)),auto:true});this.userProfile.weight=newWeight;this.saveWeightData();this.saveUserProfile();this.progress.dailyCaloriesConsumed=this.userProfile.targetCalories;this.saveProgress();console.log('Peso atualizado automaticamente:',{oldWeight:currentWeight,newWeight:newWeight.toFixed(1),change:safeWeightChange.toFixed(3),deficit:dailyDeficit.toFixed(0)});}addCaloriesConsumed(calories){if(!this.progress.dailyCaloriesConsumed){this.progress.dailyCaloriesConsumed=0;}this.progress.dailyCaloriesConsumed+=calories;this.saveProgress();this.updateUI();}resetDailyCalories(){ const today= new Date().toDateString(); const lastReset=this.progress.lastCalorieReset||'';if(lastReset!==today){this.progress.dailyCaloriesConsumed=0;this.progress.lastCalorieReset=today;this.saveProgress();}}updateStreak(){ const today= new Date().toDateString(); const lastWorkout=this.progress.lastWorkout? new Date(this.progress.lastWorkout).toDateString():null;if(!lastWorkout){this.progress.streak=1;}else{ const yesterday= new Date();yesterday.setDate(yesterday.getDate()-1);if(lastWorkout===today){return;} else if(lastWorkout===yesterday.toDateString()){this.progress.streak=(this.progress.streak||0)+1;}else{this.progress.streak=1;}}if(!this.progress.longestStreak||this.progress.streak>this.progress.longestStreak){this.progress.longestStreak=this.progress.streak;}}toggleWater(index){ const glasses=document.querySelectorAll('.glass'); const glass=glasses[index];if(glass.classList.contains('filled')){for( let i=index;i<glasses.length;i++){glasses[i].classList.remove('filled');}}else{for( let i=0;i<=index;i++){glasses[i].classList.add('filled');}this.playCuteSound('tap');} const filled=document.querySelectorAll('.glass.filled').length;document.getElementById('waterCount').textContent=filled;if(filled===8){this.playCuteSound('success');} const today= new Date().toISOString().split('T')[0];if(!this.progress.water)this.progress.water={};this.progress.water[today]=Array(filled).fill(1);this.saveProgress();this.updateDetailedStats();}startWellnessSession(type){ const sessions=this.getWellnessSessions(); const session=sessions[type];if(session){this.startWorkout(session.exercises,0);}}updateUI(){ const hour= new Date().getHours(); let greeting='Olá';if(hour<12)greeting='Bom dia'; else if(hour<18)greeting='Boa tarde'; else greeting='Boa noite'; const name=this.userProfile?this.userProfile.name.split(' ')[0]:'Guerreira';document.getElementById('greeting').textContent=`${greeting},${name}!`;if(this.userProfile&&this.userProfile.photo){ const avatar=document.querySelector('.avatar');avatar.style.backgroundImage=`url(${this.userProfile.photo})`;avatar.style.backgroundSize='cover';avatar.style.backgroundPosition='center';avatar.textContent='';}this.updatePlanSummary();document.getElementById('streakDays').textContent=this.progress.streak||0; const todayProgress=this.calculateTodayProgress();document.getElementById('progressValue').textContent=todayProgress;document.getElementById('progressFill').style.setProperty('--progress',todayProgress);document.getElementById('caloriesBurned').textContent=this.progress.totalCalories||0;document.getElementById('minutesActive').textContent=this.progress.totalMinutes||0;document.getElementById('workoutsCompleted').textContent=this.progress.workoutsCompleted||0;document.getElementById('totalWorkouts').textContent=this.progress.workoutsCompleted||0;document.getElementById('totalMinutes').textContent=this.progress.totalMinutes||0;document.getElementById('totalCalories').textContent=this.progress.totalCalories||0;document.getElementById('daysActive').textContent=this.progress.daysActive||0; const waterToday=this.getWaterToday();document.querySelectorAll('.glass').forEach((glass,index)=>{if(index<waterToday){glass.classList.add('filled');}else{glass.classList.remove('filled');}});document.getElementById('waterCount').textContent=waterToday;}calculateTodayProgress(){ const workouts=this.progress.todayWorkouts||0; const water=this.getWaterToday(); const workoutProgress=Math.min((workouts/2)*100,50); const waterProgress=Math.min((water/8)*100,50); return Math.round(workoutProgress+waterProgress);}getWaterToday(){ const today= new Date().toISOString().split('T')[0]; const waterData=this.progress.water?.[today];if(Array.isArray(waterData)){ return waterData.length;} return 0;}loadAchievements(){this.achievements=[{id:'first-workout',name:'Primeiro Passo',emoji:'👟',unlocked:(this.progress.workoutsCompleted||0)>=1},{id:'five-workouts',name:'5 Treinos',emoji:'💪',unlocked:(this.progress.workoutsCompleted||0)>=5},{id:'week-streak',name:'1 Semana',emoji:'🔥',unlocked:(this.progress.streak||0)>=7},{id:'ten-workouts',name:'10 Treinos',emoji:'🏋️',unlocked:(this.progress.workoutsCompleted||0)>=10},{id:'water-master',name:'Hidratada',emoji:'💧',unlocked:this.checkWaterStreak(5)},{id:'month-streak',name:'1 Mês',emoji:'🏆',unlocked:(this.progress.streak||0)>=30},{id:'twenty-workouts',name:'20 Treinos',emoji:'💯',unlocked:(this.progress.workoutsCompleted||0)>=20},{id:'fifty-workouts',name:'50 Treinos',emoji:'⭐',unlocked:(this.progress.workoutsCompleted||0)>=50},{id:'weight-loss',name:'Peso Perdido',emoji:'📉',unlocked:this.checkWeightLoss()},{id:'hundred-workouts',name:'100 Treinos',emoji:'🎯',unlocked:(this.progress.workoutsCompleted||0)>=100},{id:'yoga-master',name:'Mestre Yoga',emoji:'🧘‍♀️',unlocked:this.checkYogaWorkouts()},{id:'dedication',name:'Dedicação',emoji:'🌟',unlocked:(this.progress.daysActive||0)>=30}]; const container=document.getElementById('achievementsGrid');if(container){container.innerHTML='';this.achievements.forEach(achievement=>{ const card=document.createElement('div');card.className=`achievement-card ${achievement.unlocked?'':'locked'}`;card.innerHTML=`<div class="achievement-icon">${achievement.unlocked?achievement.emoji:'🔒'}</div><div class="achievement-name">${achievement.name}</div>`;container.appendChild(card);});}}checkWeightLoss(){if(this.weightData.initial&&this.weightData.current){ return this.weightData.initial>this.weightData.current;} return false;}checkYogaWorkouts(){if(!this.progress.workoutHistory) return false; const yogaWorkouts=this.progress.workoutHistory.filter(w=>w.category==='yoga'); return yogaWorkouts.length>=5;}checkAchievements(){this.loadAchievements();}checkWaterStreak(days){if(!this.progress.water) return false; let streak=0; const today= new Date();for( let i=0;i<30;i++){ const date= new Date(today);date.setDate(date.getDate()-i); const dateStr=date.toDateString();if(this.progress.water[dateStr]>=8){streak++;if(streak>=days) return true;}else{break;}} return false;}requestNotificationPermission(){if('Notification' in window&&Notification.permission!=='granted'){Notification.requestPermission().then(permission=>{if(permission==='granted'){ new Notification('Notificações Ativadas!🔔',{body:'Você receberá lembretes motivacionais!',icon:'/icons/icon.svg'});}});}}playSound(){if(this.userData.soundEnabled!==false){ const context=new(window.AudioContext||window.webkitAudioContext)(); const oscillator=context.createOscillator(); const gainNode=context.createGain();oscillator.connect(gainNode);gainNode.connect(context.destination);oscillator.frequency.value=800;oscillator.type='sine';gainNode.gain.setValueAtTime(0.3,context.currentTime);gainNode.gain.exponentialRampToValueAtTime(0.01,context.currentTime+0.1);oscillator.start(context.currentTime);oscillator.stop(context.currentTime+0.1);}}loadUserData(){ const data=localStorage.getItem('userData'); return data?JSON.parse(data):{};}loadProgress(){try{if(window.PerformanceUtils){ const data=window.PerformanceUtils.safeLocalStorageGet('userProgress',null,true);if(!data){ return this.getDefaultProgress();}if(typeof data!=='object'||data===null){console.warn('Invalid progress data,resetting'); return this.getDefaultProgress();}return{...this.getDefaultProgress(),...data};}else{ const dataString=localStorage.getItem('userProgress');if(!dataString){ return this.getDefaultProgress();} const parsed=JSON.parse(dataString);if(typeof parsed!=='object'||parsed===null){console.warn('Invalid progress data,resetting'); return this.getDefaultProgress();}return{...this.getDefaultProgress(),...parsed};}}catch(e){console.error('Error loading progress:',e); return this.getDefaultProgress();}}getDefaultProgress(){return{workoutsCompleted:0,totalMinutes:0,totalCalories:0,streak:0,daysActive:0,water:{},workoutHistory:[],todayWorkouts:0,todayMinutes:0,todayCalories:0,longestStreak:0,longestWorkout:0,memberSince:null};}saveProgress(){if(!this.saveProgressDebounced){if(window.PerformanceUtils){this.saveProgressDebounced=window.PerformanceUtils.debounce(()=>{this._doSaveProgress();},300);}else{this.saveProgressDebounced=this._doSaveProgress.bind(this);}}this.saveProgressDebounced();}_doSaveProgress(){try{if(window.PerformanceUtils){ const shouldCompress=JSON.stringify(this.progress).length>50000;window.PerformanceUtils.safeLocalStorageSet('userProgress',this.progress,shouldCompress);}else{localStorage.setItem('userProgress',JSON.stringify(this.progress));}}catch(e){console.error('Failed to save progress:',e);if(e.name==='QuotaExceededError'){this.compressProgress();}}}compressProgress(){if(this.progress.workoutHistory&&this.progress.workoutHistory.length>30){this.progress.workoutHistory=this.progress.workoutHistory.slice(-30);}if(this.progress.water){ const thirtyDaysAgo= new Date();thirtyDaysAgo.setDate(thirtyDaysAgo.getDate()-30); const recentWater={};Object.keys(this.progress.water).forEach(key=>{ const date= new Date(key);if(date>=thirtyDaysAgo){recentWater[key]=this.progress.water[key];}});this.progress.water=recentWater;}try{localStorage.setItem('userProgress',JSON.stringify(this.progress));}catch(e){console.error('Still failed after compression:',e);}}loadWeightData(){try{ const data=localStorage.getItem('weightData');if(!data){ return this.getDefaultWeightData();} const parsed=JSON.parse(data);if(typeof parsed!=='object'||parsed===null){ return this.getDefaultWeightData();}return{...this.getDefaultWeightData(),...parsed};}catch(e){console.error('Error loading weight data:',e); return this.getDefaultWeightData();}}getDefaultWeightData(){return{current:null,initial:null,goal:null,history:[]};}saveWeightData(){if(!this.saveWeightDebounced){if(window.PerformanceUtils){this.saveWeightDebounced=window.PerformanceUtils.debounce(()=>{try{window.PerformanceUtils.safeLocalStorageSet('weightData',this.weightData);}catch(e){console.error('Failed to save weight data:',e);}},300);}else{this.saveWeightDebounced=()=>{try{localStorage.setItem('weightData',JSON.stringify(this.weightData));}catch(e){console.error('Failed to save weight data:',e);}};}}this.saveWeightDebounced();}showWeightModal(){ const modal=document.getElementById('weightModal'); const weightInput=document.getElementById('weightInput'); const goalInput=document.getElementById('goalWeightInput');weightInput.value=this.weightData.current||'';goalInput.value=this.weightData.goal||'';modal.classList.add('active');this.playCuteSound('tap');}closeWeightModal(){ const modal=document.getElementById('weightModal');modal.classList.remove('active');this.playCuteSound('tap');}saveWeight(){ const weightInput=document.getElementById('weightInput'); const goalInput=document.getElementById('goalWeightInput');if(!weightInput||!goalInput){console.error('Weight inputs not found');return;} const newWeight=parseFloat(String(weightInput.value).trim()); const newGoal=parseFloat(String(goalInput.value).trim());if(isNaN(newWeight)||newWeight<30||newWeight>200){alert('Por favor,insira um peso válido entre 30 e 200 kg');return;}if(isNaN(newGoal)||newGoal<30||newGoal>200){alert('Por favor,insira uma meta válida entre 30 e 200 kg');return;}if(!Number.isFinite(newWeight)||!Number.isFinite(newGoal)){alert('Valores inválidos');return;}if(this.weightData.initial===null){this.weightData.initial=newWeight;if(!this.progress.memberSince){this.progress.memberSince= new Date().toISOString();}}this.weightData.current=newWeight;this.weightData.goal=newGoal; const today= new Date().toISOString().split('T')[0]; const existingIndex=this.weightData.history.findIndex(entry=>entry.date===today);if(existingIndex>=0){this.weightData.history[existingIndex].weight=newWeight;}else{this.weightData.history.push({date:today,weight:newWeight});}if(this.weightData.history.length>30){this.weightData.history=this.weightData.history.slice(-30);}this.saveWeightData();this.saveProgress();this.updateWeightDisplay();this.closeWeightModal();this.playCuteSound('success');}updateWeightDisplay(){ const currentWeightEl=document.getElementById('currentWeight'); const initialWeightEl=document.getElementById('initialWeight'); const goalWeightEl=document.getElementById('goalWeight'); const weightLostEl=document.getElementById('weightLost'); const weightProgressFill=document.getElementById('weightProgressFill'); const weightChartMini=document.getElementById('weightChartMini');if(this.weightData.current){currentWeightEl.textContent=`${this.weightData.current}kg`;}else{currentWeightEl.textContent='--';}if(this.weightData.initial){initialWeightEl.textContent=`${this.weightData.initial}kg`;}else{initialWeightEl.textContent='--';}if(this.weightData.goal){goalWeightEl.textContent=`${this.weightData.goal}kg`;}else{goalWeightEl.textContent='--';}if(this.weightData.initial&&this.weightData.current){ const lost=this.weightData.initial-this.weightData.current;weightLostEl.textContent=`${lost.toFixed(1)}kg`;}else{weightLostEl.textContent='0 kg';}if(this.weightData.initial&&this.weightData.current&&this.weightData.goal){ const totalToLose=this.weightData.initial-this.weightData.goal; const lost=this.weightData.initial-this.weightData.current; const percentage=Math.min(100,(lost/totalToLose)*100);weightProgressFill.style.width=`${Math.max(0,percentage)}%`;}else{weightProgressFill.style.width='0%';}if(weightChartMini&&this.weightData.history.length>0){ const maxWeight=Math.max(...this.weightData.history.map(e=>e.weight)); const minWeight=Math.min(...this.weightData.history.map(e=>e.weight)); const range=maxWeight-minWeight||1;weightChartMini.innerHTML=this.weightData.history.slice(-10).map(entry=>{ const height=((entry.weight-minWeight)/range)*80+20; return `<div class="weight-chart-bar" style="height:${height}px" title="${entry.date}:${entry.weight}kg"></div>`;}).join('');}}updateDetailedStats(){document.getElementById('totalWorkouts').textContent=this.progress.workoutsCompleted||0;document.getElementById('totalMinutes').textContent=this.progress.totalMinutes||0;document.getElementById('totalCaloriesDetail').textContent=this.progress.totalCalories||0; const uniqueDays= new Set();if(this.progress.workoutHistory){this.progress.workoutHistory.forEach(w=>{ const date= new Date(w.date).toDateString();uniqueDays.add(date);});}this.progress.daysActive=uniqueDays.size;document.getElementById('daysActiveDetail').textContent=this.progress.daysActive||0;document.getElementById('currentStreak').textContent=this.progress.streak||0; const thisWeek=this.getThisWeekWorkouts();document.getElementById('thisWeekWorkouts').textContent=thisWeek; const avgMinutes=this.progress.workoutsCompleted>0?Math.round(this.progress.totalMinutes/this.progress.workoutsCompleted):0; const avgCalories=this.progress.workoutsCompleted>0?Math.round(this.progress.totalCalories/this.progress.workoutsCompleted):0;document.getElementById('avgMinutes').textContent=avgMinutes;document.getElementById('avgCalories').textContent=avgCalories; const totalWaterGlasses=Object.values(this.progress.water||{}).reduce((sum,day)=>{ return sum+(Array.isArray(day)?day.length:0);},0); const waterStreak=this.getWaterStreak();document.getElementById('totalWaterGlasses').textContent=totalWaterGlasses;document.getElementById('waterStreak').textContent=waterStreak;if(!this.achievements){this.loadAchievements();} const unlockedAchievements=this.achievements.filter(a=>a.unlocked).length;document.getElementById('achievementsUnlocked').textContent=unlockedAchievements;document.getElementById('totalAchievements').textContent=this.achievements.length;document.getElementById('longestStreak').textContent=`${this.progress.longestStreak||0}dias`;document.getElementById('longestWorkout').textContent=`${this.progress.longestWorkout||0}min`; const favoriteCategory=this.getFavoriteCategory();document.getElementById('favoriteCategory').textContent=favoriteCategory;if(this.progress.memberSince){ const date= new Date(this.progress.memberSince); const formatted=date.toLocaleDateString('pt-BR');document.getElementById('memberSince').textContent=formatted;}else{document.getElementById('memberSince').textContent='--';}this.renderWeeklyChart();}getThisWeekWorkouts(){if(!this.progress.workoutHistory) return 0; const oneWeekAgo= new Date();oneWeekAgo.setDate(oneWeekAgo.getDate()-7); return this.progress.workoutHistory.filter(workout=>{ const workoutDate= new Date(workout.date); return workoutDate>=oneWeekAgo;}).length;}getWaterStreak(){if(!this.progress.water) return 0; let streak=0; const today= new Date();for( let i=0;i<365;i++){ const date= new Date(today);date.setDate(date.getDate()-i); const dateKey=date.toISOString().split('T')[0];if(this.progress.water[dateKey]&&this.progress.water[dateKey].length>=8){streak++;}else{break;}} return streak;}getFavoriteCategory(){if(!this.progress.workoutHistory||this.progress.workoutHistory.length===0){ return '--';} const categoryCount={};this.progress.workoutHistory.forEach(workout=>{categoryCount[workout.category]=(categoryCount[workout.category]||0)+1;}); const favorite=Object.keys(categoryCount).reduce((a,b)=>categoryCount[a]>categoryCount[b]?a:b); const categoryNames={abs:'Abdômen',legs:'Pernas',glutes:'Glúteos',arms:'Braços',waist:'Cintura',back:'Costas',cardio:'Cardio',fullbody:'Corpo Todo',yoga:'Yoga',massage:'Massagem'}; return categoryNames[favorite]||favorite;}renderWeeklyChart(){ const chartContainer=document.getElementById('weeklyChart');if(!chartContainer)return; const days=['Dom','Seg','Ter','Qua','Qui','Sex','Sáb']; const today= new Date(); const weekData=[];for( let i=6;i>=0;i--){ const date= new Date(today);date.setDate(date.getDate()-i); const dayIndex=date.getDay(); const dateKey=date.toISOString().split('T')[0]; const workoutsOnDay=this.progress.workoutHistory?this.progress.workoutHistory.filter(w=>w.date.startsWith(dateKey)).length:0;weekData.push({label:days[dayIndex],value:workoutsOnDay});} const maxValue=Math.max(...weekData.map(d=>d.value),1);chartContainer.innerHTML=weekData.map(day=>{ const heightPercent=(day.value/maxValue)*100; return `<div class="chart-day"><div class="chart-bar" style="height:${heightPercent}%"></div><div class="chart-label">${day.label}</div></div>`;}).join('');}getExercisesByCategory(category){ const exercises={personalized:[{name:'Prancha com Balanço Lateral',emoji:'🔥',video:'videos/Prancha com balanço lateral.mp4',sets:3,reps:'10',rest:30,calories:8},{name:'Prancha com Elevação de Perna',emoji:'🔥',video:'videos/Prancha com elevação de perna.mp4',sets:3,reps:'12',rest:30,calories:10},{name:'Tesoura(Scissor Kicks)',emoji:'✂️',video:'videos/Scissor Kicks.mp4',sets:3,reps:'15',rest:30,calories:9},{name:'Elevação Alternada de Pernas',emoji:'🦵',video:'videos/Alternating Leg Lifts.mp4',sets:3,reps:'12',rest:30,calories:8},{name:'Prancha Lateral com Rotação',emoji:'🔄',video:'videos/prancha lateral com rotação.mp4',sets:3,reps:'10 cada',rest:30,calories:10},{name:'Face Esculpida',emoji:'😊',video:'videos/Sculpted face.mp4',sets:3,reps:'10',rest:20,calories:3},{name:'Face Afinada',emoji:'✨',video:'videos/Slim face.mp4',sets:3,reps:'12',rest:20,calories:3},{name:'Redução de Linhas de Sorriso',emoji:'😄',video:'videos/Smile lines.mp4',sets:3,reps:'10',rest:20,calories:2},{name:'Redução de Papada',emoji:'💆‍♀️',video:'videos/Double chin.mp4',sets:3,reps:'15',rest:20,calories:4},{name:'Alongamento Lateral Dinâmico',emoji:'⏳',video:'videos/Dynamic Side Stretch].mp4',sets:3,reps:'12 cada',rest:25,calories:6},{name:'Torção do Tronco Sentado',emoji:'🔄',video:'videos/Seated Torso Twist.mp4',sets:3,reps:'15 cada',rest:25,calories:7},{name:'Flexão Lateral em Pé',emoji:'⏳',video:'videos/Side Bend Stretch.mp4',sets:3,reps:'12 cada',rest:25,calories:6},{name:'Agachamento com Rotação',emoji:'🏋️‍♀️',video:'videos/agachamento rotação.mp4',sets:3,reps:'12',rest:35,calories:12},{name:'Flexão e Extensão do Tronco',emoji:'🧘‍♀️',video:'videos/flexao,extensão e hiperextenxao do tronco com baco estendido.mp4',sets:3,reps:'10',rest:30,calories:8}],abs:[{name:'Prancha',emoji:'🧘‍♀️',sets:3,reps:'30s',rest:30},{name:'Abdominal Tradicional',emoji:'💪',sets:3,reps:'15',rest:30},{name:'Bicicleta',emoji:'🚴‍♀️',sets:3,reps:'20',rest:30},{name:'Prancha Lateral',emoji:'🧘‍♀️',sets:3,reps:'20s cada',rest:30},{name:'Mountain Climbers',emoji:'⛰️',sets:3,reps:'15',rest:30}],legs:[{name:'Agachamento',emoji:'🏋️‍♀️',sets:3,reps:'15',rest:30},{name:'Afundo',emoji:'🦵',sets:3,reps:'12 cada',rest:30},{name:'Elevação de Panturrilha',emoji:'👠',sets:3,reps:'20',rest:30},{name:'Agachamento Sumô',emoji:'🏋️‍♀️',sets:3,reps:'15',rest:30},{name:'Ponte com Uma Perna',emoji:'🌉',sets:3,reps:'10 cada',rest:30}],face:[{name:'Massagem Facial-Testa',emoji:'💆‍♀️',sets:1,reps:'1min',rest:10},{name:'Massagem Facial-Têmporas',emoji:'😌',sets:1,reps:'30s',rest:10},{name:'Massagem Facial-Bochechas',emoji:'😊',sets:1,reps:'1min',rest:10},{name:'Elevação de Sobrancelhas',emoji:'👁️',sets:2,reps:'10',rest:15}],glutes:[{name:'Ponte',emoji:'🍑',sets:3,reps:'15',rest:30},{name:'Chute de Glúteo',emoji:'🦵',sets:3,reps:'15 cada',rest:30},{name:'Agachamento com Salto',emoji:'⚡',sets:3,reps:'12',rest:40},{name:'Elevação de Quadril',emoji:'🏋️‍♀️',sets:3,reps:'15',rest:30},{name:'Concha(Clamshell)',emoji:'🐚',sets:3,reps:'15 cada',rest:30}],arms:[{name:'Flexão de Braço',emoji:'💪',sets:3,reps:'10',rest:30},{name:'Tríceps no Banco',emoji:'🪑',sets:3,reps:'12',rest:30},{name:'Rosca Direta',emoji:'💪',sets:3,reps:'15',rest:30},{name:'Prancha com Toque',emoji:'🧘‍♀️',sets:3,reps:'10',rest:30}],waist:[{name:'Oblíquo em Pé',emoji:'⏳',sets:3,reps:'15 cada',rest:30},{name:'Twist Russo',emoji:'🔄',sets:3,reps:'20',rest:30},{name:'Prancha com Rotação',emoji:'🧘‍♀️',sets:3,reps:'10 cada',rest:30},{name:'Elevação de Perna Lateral',emoji:'🦵',sets:3,reps:'15 cada',rest:30}],back:[{name:'Superman',emoji:'🦸‍♀️',sets:3,reps:'12',rest:30},{name:'Remada Curvada',emoji:'🏋️‍♀️',sets:3,reps:'15',rest:30},{name:'Prancha Reversa',emoji:'🧘‍♀️',sets:3,reps:'30s',rest:30},{name:'Extensão Lombar',emoji:'💪',sets:3,reps:'12',rest:30}],cardio:[{name:'Polichinelo',emoji:'🤸‍♀️',sets:3,reps:'30',rest:20},{name:'Burpees',emoji:'💥',sets:3,reps:'10',rest:40},{name:'High Knees',emoji:'🏃‍♀️',sets:3,reps:'30s',rest:20},{name:'Mountain Climbers',emoji:'⛰️',sets:3,reps:'30s',rest:20},{name:'Pular Corda',emoji:'🪢',sets:3,reps:'1min',rest:30}],fullbody:[{name:'Burpees',emoji:'💥',sets:3,reps:'10',rest:40},{name:'Agachamento',emoji:'🏋️‍♀️',sets:3,reps:'15',rest:30},{name:'Flexão',emoji:'💪',sets:3,reps:'10',rest:30},{name:'Prancha',emoji:'🧘‍♀️',sets:3,reps:'40s',rest:30},{name:'Mountain Climbers',emoji:'⛰️',sets:3,reps:'20',rest:30}],yoga:[{name:'Postura da Criança',emoji:'🧘‍♀️',sets:1,reps:'1min',rest:20},{name:'Cachorro Olhando para Baixo',emoji:'🐕',sets:2,reps:'30s',rest:20},{name:'Guerreiro I',emoji:'⚔️',sets:2,reps:'30s cada',rest:20},{name:'Guerreiro II',emoji:'🗡️',sets:2,reps:'30s cada',rest:20},{name:'Árvore',emoji:'🌳',sets:2,reps:'30s cada',rest:20},{name:'Cobra',emoji:'🐍',sets:2,reps:'30s',rest:20},{name:'Ponte',emoji:'🌉',sets:2,reps:'30s',rest:20},{name:'Triângulo',emoji:'📐',sets:2,reps:'30s cada',rest:20},{name:'Torção Sentada',emoji:'🔄',sets:2,reps:'30s cada',rest:20},{name:'Gato-Vaca',emoji:'🐱',sets:2,reps:'10',rest:15},{name:'Prancha Lateral',emoji:'🧘‍♀️',sets:2,reps:'20s cada',rest:20},{name:'Savasana(Relaxamento)',emoji:'😌',sets:1,reps:'2min',rest:0}],massage:[{name:'Massagem Facial-Testa',emoji:'💆‍♀️',sets:1,reps:'1min',rest:10},{name:'Massagem Facial-Têmporas',emoji:'😌',sets:1,reps:'30s',rest:10},{name:'Massagem Facial-Bochechas',emoji:'😊',sets:1,reps:'1min',rest:10},{name:'Massagem Pescoço',emoji:'💫',sets:1,reps:'1min',rest:15},{name:'Massagem Ombros',emoji:'🤗',sets:1,reps:'1min cada',rest:15},{name:'Massagem Mãos',emoji:'🙌',sets:1,reps:'30s cada',rest:10},{name:'Massagem Pés',emoji:'👣',sets:1,reps:'1min cada',rest:15},{name:'Relaxamento Completo',emoji:'✨',sets:1,reps:'2min',rest:0}]}; return exercises[category]||[];}getWellnessSessions(){return{'face-massage':{exercises:[{name:'Massagem na Testa',emoji:'💆‍♀️',sets:1,reps:'30s',rest:10},{name:'Contorno dos Olhos',emoji:'👁️',sets:1,reps:'30s',rest:10},{name:'Bochechas',emoji:'😊',sets:1,reps:'30s',rest:10},{name:'Mandíbula',emoji:'💫',sets:1,reps:'30s',rest:10}]},'body-massage':{exercises:[{name:'Pescoço e Ombros',emoji:'💆‍♀️',sets:1,reps:'1min',rest:20},{name:'Braços',emoji:'💪',sets:1,reps:'30s cada',rest:10},{name:'Pernas',emoji:'🦵',sets:1,reps:'1min cada',rest:20},{name:'Pés',emoji:'👣',sets:1,reps:'30s cada',rest:10}]},'posture':{exercises:[{name:'Alongamento Cervical',emoji:'🧍‍♀️',sets:2,reps:'20s',rest:10},{name:'Abertura de Peito',emoji:'💫',sets:2,reps:'30s',rest:10},{name:'Gato-Vaca',emoji:'🐱',sets:2,reps:'10',rest:10},{name:'Postura na Parede',emoji:'🧱',sets:1,reps:'1min',rest:0}]},'stretching':{exercises:[{name:'Alongamento de Pernas',emoji:'🦵',sets:2,reps:'30s cada',rest:10},{name:'Alongamento de Braços',emoji:'💪',sets:2,reps:'20s cada',rest:10},{name:'Torção Espinal',emoji:'🔄',sets:2,reps:'20s cada',rest:10},{name:'Alongamento Total',emoji:'🤸‍♀️',sets:1,reps:'30s',rest:0}]},'breathing':{exercises:[{name:'Respiração Profunda',emoji:'🌬️',sets:3,reps:'10',rest:20},{name:'Respiração Alternada',emoji:'👃',sets:2,reps:'5 cada',rest:20},{name:'Respiração 4-7-8',emoji:'💫',sets:3,reps:'4',rest:10}]},'meditation':{exercises:[{name:'Meditação Guiada',emoji:'🧘‍♀️',sets:1,reps:'5min',rest:0},{name:'Visualização',emoji:'✨',sets:1,reps:'3min',rest:0},{name:'Gratidão',emoji:'🙏',sets:1,reps:'2min',rest:0}]}};}}document.addEventListener('DOMContentLoaded',()=>{window.app= new FitnessApp();if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js').then(reg=>console.log('Service Worker registered:',reg.scope)).catch(err=>console.error('Service Worker registration failed:',err));}}); const style=document.createElement('style');style.textContent=` @keyframes fadeOut{from{opacity:1;}to{opacity:0;}}`;document.head.appendChild(style);
dist/app.min.js.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6495a08a1503b0750ae40a0d291f2d9501c260cb62c5d4ab0556eb9796a928f1
3
+ size 15755
dist/build-report.json ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "buildDate": "2025-10-16T10:10:38.286Z",
3
+ "files": [
4
+ {
5
+ "name": "JavaScript",
6
+ "originalSize": 103828,
7
+ "minifiedSize": 63924,
8
+ "gzipSize": 15755,
9
+ "minSavings": "38.4%",
10
+ "gzipSavings": "84.8%"
11
+ },
12
+ {
13
+ "name": "CSS",
14
+ "originalSize": 45538,
15
+ "minifiedSize": 33154,
16
+ "gzipSize": 5741,
17
+ "minSavings": "27.2%",
18
+ "gzipSavings": "87.4%"
19
+ },
20
+ {
21
+ "name": "HTML",
22
+ "originalSize": 35221,
23
+ "minifiedSize": 19069,
24
+ "gzipSize": 4981,
25
+ "minSavings": "45.9%",
26
+ "gzipSavings": "85.9%"
27
+ },
28
+ {
29
+ "name": "Service Worker",
30
+ "originalSize": 4215,
31
+ "minifiedSize": 2030,
32
+ "gzipSize": 730,
33
+ "minSavings": "51.8%",
34
+ "gzipSavings": "82.7%"
35
+ }
36
+ ],
37
+ "totals": {
38
+ "originalSize": 188802,
39
+ "minifiedSize": 118177,
40
+ "gzipSize": 27207,
41
+ "minSavings": "37.4%",
42
+ "gzipSavings": "85.6%"
43
+ },
44
+ "performance": {
45
+ "estimatedLoadTime3G": "0.28s",
46
+ "estimatedLoadTime4G": "0.04s"
47
+ }
48
+ }
dist/index.html ADDED
@@ -0,0 +1 @@
 
 
1
+ <!DOCTYPE html><html lang=pt-BR><head><meta charset=UTF-8><meta name=viewport content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover"><meta name=mobile-web-app-capable content=yes><meta name=apple-mobile-web-app-capable content=yes><meta name=apple-mobile-web-app-title content="Fitness App"><title>✨ Sua Jornada de Transformação</title><link rel=dns-prefetch href="https://fonts.googleapis.com"><link rel=dns-prefetch href="https://fonts.gstatic.com"><link rel=preconnect href="https://fonts.googleapis.com"><link rel=preconnect href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&family=Playfair+Display:wght@400;600;700&display=swap" rel=stylesheet media=print onload="this.media='all'"><noscript><link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&family=Playfair+Display:wght@400;600;700&display=swap" rel=stylesheet></noscript><link rel=preload href="styles.css" as=style><link rel=stylesheet href="styles.css"><link rel=modulepreload href="app.js"><link rel=modulepreload href="lazy-video.js"><link rel=manifest href="manifest.json"><meta name=description content="Seu aplicativo de transformação pessoal completo - exercícios, nutrição e bem-estar"><meta name=keywords content="fitness, treino, nutrição, bem-estar, yoga, meditação, saúde"><meta name=theme-color content="#FF6B9D"><meta name=apple-mobile-web-app-capable content=yes><meta name=apple-mobile-web-app-status-bar-style content=black-translucent><link rel=icon type="image/svg+xml" href="icons/icon.svg"><link rel=preload href="app.js" as=script><link rel=modulepreload href="app.js"></head><body><div class=welcome-screen id=welcomeScreen><div class=welcome-content><div class=welcome-logo><div class=logo-heart>💖</div><h1>Bem-vinda!</h1><p>Sua jornada de transformação começa aqui</p></div><button class=btn-start-journey id=startJourney>Começar Agora ✨</button></div></div><div class=app-container id=appContainer style="display: none;"><header class=top-bar><div class=user-info><div class=avatar>💝</div><div class=user-text><span class=greeting id=greeting>Olá, Guerreira!</span><span class=streak>🔥 <span id=streakDays>0</span> dias seguidos</span></div></div><div class=top-actions><button class=icon-btn id=notifBtn title="Notificações"><span>🔔</span></button></div></header><main class=main-view><section class="view active" id=homeView><div class=hero-section><h2 class=page-title>Sua Transformação</h2><div class=daily-progress><div class=progress-circle id=progressCircle><svg viewBox="0 0 120 120"><circle cx=60 cy=60 r=54 class=progress-bg></circle><circle cx=60 cy=60 r=54 class=progress-fill id=progressFill style="--progress: 0"></circle></svg><div class=progress-text><span class=progress-value id=progressValue>0</span>% <span class=progress-label>Hoje</span></div></div><div class=today-stats><div class=stat><span class=stat-icon>🔥</span><div><span class=stat-value id=caloriesBurned>0</span><span class=stat-label>kcal</span></div></div><div class=stat><span class=stat-icon>⏱️</span><div><span class=stat-value id=minutesActive>0</span><span class=stat-label>min</span></div></div><div class=stat><span class=stat-icon>💪</span><div><span class=stat-value id=workoutsCompleted>0</span><span class=stat-label>treinos</span></div></div></div></div></div><div class=plan-summary id=planSummary style="display: none;"><div class=plan-card><div class=plan-header><h3>🎯 Seu Plano Personalizado</h3><button class=btn-view-plan id=viewFullPlan>Ver Detalhes</button></div><div class=plan-quick-stats><div class=plan-stat><span class=plan-label>Meta</span><span class=plan-value id=planGoal>-</span></div><div class=plan-stat><span class=plan-label>Calorias/dia</span><span class=plan-value id=planCalories>-</span></div><div class=plan-stat><span class=plan-label>Treino</span><span class=plan-value id=planWorkout>-</span></div></div><div class=coach-message id=coachMessage> 💪 Continue assim! Você está no caminho certo! </div></div></div><div class=quick-actions><h3 class=section-title>Comece Agora</h3><div class=action-cards><div class=action-card data-navigate="workouts"><div class=action-icon>💪</div><h4>Treinar</h4><p>Escolha sua área</p></div><div class=action-card data-navigate="nutrition"><div class=action-icon>🥗</div><h4>Nutrição</h4><p>Acompanhe sua dieta</p></div><div class=action-card data-navigate="wellness"><div class=action-icon>🧘‍♀️</div><h4>Bem-Estar</h4><p>Massagens & Postura</p></div><div class=action-card data-navigate="progress"><div class=action-icon>📈</div><h4>Progresso</h4><p>Veja sua evolução</p></div></div></div><div class=motivation-card><div class=motivation-icon>✨</div><p class=motivation-text id=dailyMotivation>Você é capaz de coisas incríveis! Continue brilhando! 💫</p></div></section><section class=view id=workoutsView><div class=view-header><button class=btn-back data-back="home">← Voltar</button><h2 class=view-title>Escolha a Área</h2></div><div class=category-grid><div class=category-card data-category="personalized" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;"><div class=category-image>🎯</div><h3>Personalizado</h3><p>Seu treino único</p><span class=category-badge style="background: rgba(255,255,255,0.3);">14 exercícios</span></div><div class=category-card data-category="abs"><div class=category-image>🔥</div><h3>Abdômen</h3><p>Barriga definida</p><span class=category-badge>5 exercícios</span></div><div class=category-card data-category="legs"><div class=category-image>🦵</div><h3>Pernas</h3><p>Pernas torneadas</p><span class=category-badge>15 exercícios</span></div><div class=category-card data-category="glutes"><div class=category-image>🍑</div><h3>Glúteos</h3><p>Bumbum empinado</p><span class=category-badge>14 exercícios</span></div><div class=category-card data-category="arms"><div class=category-image>💪</div><h3>Braços</h3><p>Braços definidos</p><span class=category-badge>10 exercícios</span></div><div class=category-card data-category="waist"><div class=category-image>⏳</div><h3>Cintura</h3><p>Cintura fina</p><span class=category-badge>8 exercícios</span></div><div class=category-card data-category="back"><div class=category-image>🏋️‍♀️</div><h3>Costas</h3><p>Postura correta</p><span class=category-badge>9 exercícios</span></div><div class=category-card data-category="cardio"><div class=category-image>❤️</div><h3>Cardio</h3><p>Queime calorias</p><span class=category-badge>11 exercícios</span></div><div class=category-card data-category="fullbody"><div class=category-image>✨</div><h3>Corpo Todo</h3><p>Treino completo</p><span class=category-badge>20 exercícios</span></div><div class=category-card data-category="yoga"><div class=category-image>🧘‍♀️</div><h3>Yoga</h3><p>Flexibilidade e paz</p><span class=category-badge>12 posições</span></div><div class=category-card data-category="massage"><div class=category-image>💆‍♀️</div><h3>Massagem</h3><p>Relaxamento total</p><span class=category-badge>8 técnicas</span></div><div class=category-card data-category="face"><div class=category-image>😊</div><h3>Rosto</h3><p>Rejuvenescimento facial</p><span class=category-badge>4 exercícios</span></div></div></section><section class=view id=exercisesListView><div class=view-header><button class=btn-back data-back="workouts">← Voltar</button><h2 class=view-title id=categoryTitle>Exercícios</h2></div><div class=exercises-container id=exercisesList></div></section><section class=view id=workoutSessionView><div class=workout-header><button class=btn-close-workout id=closeWorkout>✕</button><div class=workout-timer id=workoutTimer>00:00</div></div><div class=workout-content><div class=exercise-display><div class=exercise-name id=currentExerciseName>Preparando...</div><div class=exercise-count id=exerciseCount>Exercício 1 de 5</div><div class=exercise-demo id=exerciseDemo><div class=demo-placeholder><video class=demo-video id=demoVideo loop muted playsinline style="display: none;"><source src="" type="video/mp4"></video><span class=demo-icon id=demoIcon>💪</span></div></div><div class=exercise-instructions><div class=reps-info id=repsInfo>3 séries × 15 repetições</div><div class=rest-info id=restInfo>Descanso: 30s entre séries</div></div><div class=series-tracker id=seriesTracker><div class="series-dot completed"></div><div class=series-dot></div><div class=series-dot></div></div></div><div class=workout-controls><button class="btn-workout-action secondary" id=skipExercise>Pular</button><button class="btn-workout-action primary" id=completeExercise>Concluir Série</button></div></div><div class=workout-progress-bar><div class=progress-bar-fill id=workoutProgressBar style="width: 0%"></div></div></section><section class=view id=wellnessView><div class=view-header><button class=btn-back data-back="home">← Voltar</button><h2 class=view-title>Bem-Estar</h2></div><div class=wellness-grid><div class=wellness-card data-wellness="face-massage"><div class=wellness-icon>💆‍♀️</div><h3>Massagem Facial</h3><p>Rejuvenesça e relaxe</p><span class=duration>10 min</span></div><div class=wellness-card data-wellness="body-massage"><div class=wellness-icon>💫</div><h3>Massagem Corporal</h3><p>Alivie tensões</p><span class=duration>15 min</span></div><div class=wellness-card data-wellness="posture"><div class=wellness-icon>🧍‍♀️</div><h3>Correção Postural</h3><p>Melhore sua postura</p><span class=duration>12 min</span></div><div class=wellness-card data-wellness="stretching"><div class=wellness-icon>🤸‍♀️</div><h3>Alongamento</h3><p>Flexibilidade total</p><span class=duration>8 min</span></div><div class=wellness-card data-wellness="breathing"><div class=wellness-icon>🌬️</div><h3>Respiração</h3><p>Acalme sua mente</p><span class=duration>5 min</span></div><div class=wellness-card data-wellness="meditation"><div class=wellness-icon>🧘‍♀️</div><h3>Meditação</h3><p>Paz interior</p><span class=duration>10 min</span></div></div></section><section class=view id=nutritionView><div class=view-header><button class=btn-back data-back="home">← Voltar</button><h2 class=view-title>Nutrição</h2></div><div class=nutrition-summary><h3>Resumo de Hoje</h3><div class=macros-display><div class=macro-item><div class="macro-circle carbs"><span id=carbsValue>0g</span></div><span class=macro-label>Carboidratos</span></div><div class=macro-item><div class="macro-circle protein"><span id=proteinValue>0g</span></div><span class=macro-label>Proteínas</span></div><div class=macro-item><div class="macro-circle fat"><span id=fatValue>0g</span></div><span class=macro-label>Gorduras</span></div></div><div class=calories-total><span class=calories-value id=totalCalories>0</span><span class=calories-label>kcal hoje</span></div></div><div class=water-tracker><h3>Hidratação 💧</h3><div class=water-glasses><div class=glass data-glass="1">💧</div><div class=glass data-glass="2">💧</div><div class=glass data-glass="3">💧</div><div class=glass data-glass="4">💧</div><div class=glass data-glass="5">💧</div><div class=glass data-glass="6">💧</div><div class=glass data-glass="7">💧</div><div class=glass data-glass="8">💧</div></div><p class=water-goal><span id=waterCount>0</span>/8 copos (2L)</p></div></section><section class=view id=progressView><div class=view-header><button class=btn-back data-back="home">← Voltar</button><h2 class=view-title>Seu Progresso</h2></div><div class=weight-tracking-section><h3>Controle de Peso ⚖️</h3><div class=weight-card><div class=weight-current><div class=weight-label>Peso Atual</div><div class=weight-value id=currentWeight>--</div><button class=btn-update-weight id=updateWeightBtn>Atualizar Peso</button></div><div class=weight-stats><div class=weight-stat><div class=weight-stat-label>Peso Inicial</div><div class=weight-stat-value id=initialWeight>--</div></div><div class="weight-stat success"><div class=weight-stat-label>Perdeu</div><div class=weight-stat-value id=weightLost>0 kg</div></div><div class=weight-stat><div class=weight-stat-label>Meta</div><div class=weight-stat-value id=goalWeight>--</div></div></div><div class=weight-progress-bar><div class=weight-progress-fill id=weightProgressFill style="width: 0%"></div></div><div class=weight-chart-mini id=weightChartMini></div></div></div><div class=detailed-stats-section><h3>Estatísticas Detalhadas 📊</h3><div class=stats-grid><div class=stat-detail-card><div class=stat-detail-icon>🔥</div><div class=stat-detail-content><div class=stat-detail-number id=totalWorkouts>0</div><div class=stat-detail-label>Treinos Completos</div><div class=stat-detail-sublabel><span id=thisWeekWorkouts>0</span> esta semana </div></div></div><div class=stat-detail-card><div class=stat-detail-icon>⏱️</div><div class=stat-detail-content><div class=stat-detail-number id=totalMinutes>0</div><div class=stat-detail-label>Minutos Ativos</div><div class=stat-detail-sublabel><span id=avgMinutes>0</span> min/treino </div></div></div><div class=stat-detail-card><div class=stat-detail-icon>🔥</div><div class=stat-detail-content><div class=stat-detail-number id=totalCaloriesDetail>0</div><div class=stat-detail-label>Calorias Queimadas</div><div class=stat-detail-sublabel><span id=avgCalories>0</span> kcal/treino </div></div></div><div class=stat-detail-card><div class=stat-detail-icon>📅</div><div class=stat-detail-content><div class=stat-detail-number id=daysActiveDetail>0</div><div class=stat-detail-label>Dias Ativos</div><div class=stat-detail-sublabel> Sequência: <span id=currentStreak>0</span> dias </div></div></div><div class=stat-detail-card><div class=stat-detail-icon>💧</div><div class=stat-detail-content><div class=stat-detail-number id=totalWaterGlasses>0</div><div class=stat-detail-label>Copos de Água</div><div class=stat-detail-sublabel><span id=waterStreak>0</span> dias 8 copos </div></div></div><div class=stat-detail-card><div class=stat-detail-icon>🏆</div><div class=stat-detail-content><div class=stat-detail-number id=achievementsUnlocked>0</div><div class=stat-detail-label>Conquistas</div><div class=stat-detail-sublabel> de <span id=totalAchievements>12</span> possíveis </div></div></div></div></div><div class=activity-chart-section><h3>Atividade Semanal 📈</h3><div class=weekly-chart><div class=chart-bars id=weeklyChart></div></div></div><div class=achievements-section><h3>Conquistas 🏆</h3><div class=achievements-grid id=achievementsGrid></div></div><div class=records-section><h3>Recordes Pessoais 🌟</h3><div class=records-list><div class=record-item><div class=record-icon>🔥</div><div class=record-content><div class=record-label>Maior Sequência</div><div class=record-value id=longestStreak>0 dias</div></div></div><div class=record-item><div class=record-icon>⏱️</div><div class=record-content><div class=record-label>Treino Mais Longo</div><div class=record-value id=longestWorkout>0 min</div></div></div><div class=record-item><div class=record-icon>💪</div><div class=record-content><div class=record-label>Categoria Favorita</div><div class=record-value id=favoriteCategory>--</div></div></div><div class=record-item><div class=record-icon>📅</div><div class=record-content><div class=record-label>Membro Desde</div><div class=record-value id=memberSince>--</div></div></div></div></div></section></main><nav class=bottom-nav><button class="nav-item active" data-nav="home"><span class=nav-icon>🏠</span><span class=nav-label>Início</span></button><button class=nav-item data-nav="workouts"><span class=nav-icon>💪</span><span class=nav-label>Treinar</span></button><button class=nav-item data-nav="nutrition"><span class=nav-icon>🥗</span><span class=nav-label>Nutrição</span></button><button class=nav-item data-nav="progress"><span class=nav-icon>📊</span><span class=nav-label>Progresso</span></button></nav></div><div class=modal id=completionModal><div class="modal-content celebration"><div class=celebration-confetti>🎉</div><h2 class=modal-title>Parabéns! 🎊</h2><p class=modal-message id=completionMessage>Você completou o treino!</p><div class=workout-summary id=workoutSummary><div class=summary-stat><span class=summary-icon>⏱️</span><span class=summary-value id=summaryTime>15 min</span></div><div class=summary-stat><span class=summary-icon>🔥</span><span class=summary-value id=summaryCalories>120 kcal</span></div><div class=summary-stat><span class=summary-icon>💪</span><span class=summary-value id=summaryExercises>8 exercícios</span></div></div><div class=workout-details id=workoutDetails style="margin-top: var(--spacing-md); padding: var(--spacing-md); background: var(--bg-light); border-radius: var(--radius-md); text-align: left; font-size: 0.9rem; color: var(--text-secondary);"></div><button class=btn-modal-primary id=closeCompletionModal>Continuar ✨</button></div></div><div class=modal id=weightModal><div class=modal-content><h2 class=modal-title>Atualizar Peso ⚖️</h2><div class=weight-input-group><label for=weightInput>Seu peso atual (kg)</label><input type=number id=weightInput step="0.1" min=30 max=200 placeholder="Ex: 65.5"></div><div class=weight-input-group><label for=goalWeightInput>Seu peso meta (kg)</label><input type=number id=goalWeightInput step="0.1" min=30 max=200 placeholder="Ex: 60.0"></div><div class=modal-actions><button class=btn-modal-secondary id=cancelWeightBtn>Cancelar</button><button class=btn-modal-primary id=saveWeightBtn>Salvar 💖</button></div></div></div><div class=settings-fab id=settingsFab><button class=fab-settings id=toggleSound><span class=fab-icon id=soundIcon>🔊</span></button></div><link rel=preload href="lazy-video.js" as=script><script src="lazy-video.js" defer></script><script> // Only load performance monitor in dev mode if (location.hostname === 'localhost' || location.search.includes('debug=true')) { const script = document.createElement('script'); script.src = 'performance-monitor.js'; script.defer = true; document.head.appendChild(script); } </script><div class=modal id=planModal><div class="modal-content plan-modal-content"><button class=modal-close onclick="app.closeModal('planModal')">×</button><div id=planModalContent><h2 class=modal-title>🎯 Seu Plano Completo</h2><div class=profile-info id=profileInfo></div><div class=plan-section><h3>🍽️ Plano Nutricional</h3><div id=nutritionPlan></div></div><div class=plan-section><h3>💪 Plano de Treino</h3><div id=workoutPlan></div></div><div class=plan-section><h3>📅 Linha do Tempo</h3><div id=timelinePlan></div></div><div class=plan-section><h3>💡 Dicas Personalizadas</h3><div id=tipsPlan></div></div><button class=btn-edit-profile id=editProfile>✏️ Editar Perfil</button></div></div></div><script src="app.js" defer></script></body></html>
dist/index.html.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4a43a869f58883288d5d32f6324de827580dce4e3911dbb3f638fa1680c775f9
3
+ size 4981
dist/styles.min.css ADDED
@@ -0,0 +1 @@
 
 
1
+ :root{--primary:#FF6B9D;--primary-dark:#E91E63;--secondary:#9C27B0;--accent:#FFB6C1;--success:#4CAF50;--warning:#FF9800;--gradient-primary:linear-gradient(135deg,#FF6B9D 0,#C2185B 100%);--gradient-secondary:linear-gradient(135deg,#9C27B0 0,#7B1FA2 100%);--gradient-soft:linear-gradient(135deg,#FFE5EC 0,#FFF0F5 100%);--gradient-hero:linear-gradient(135deg,#FF6B9D 0,#9C27B0 100%);--white:#FFF;--bg-light:#FFF5F8;--bg-card:#FFF;--text-primary:#2D3748;--text-secondary:#718096;--border:#FFE5EC;--shadow-sm:0 2px 8px rgba(255,107,157,0.1);--shadow-md:0 4px 16px rgba(255,107,157,0.15);--shadow-lg:0 8px 24px rgba(255,107,157,0.2);--shadow-xl:0 12px 32px rgba(255,107,157,0.25);--spacing-xs:4px;--spacing-sm:8px;--spacing-md:16px;--spacing-lg:24px;--spacing-xl:32px;--radius-sm:8px;--radius-md:16px;--radius-lg:24px;--radius-full:9999px}*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent}body{font-family:'Poppins',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg-light);color:var(--text-primary);overflow-x:hidden;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-webkit-overflow-scrolling:touch;text-rendering:optimizeLegibility}.profile-setup-screen{position:fixed;top:0;left:0;width:100%;height:100vh;background:var(--bg-light);overflow-y:auto;z-index:10000;padding:var(--spacing-lg)}.profile-setup-content{max-width:500px;margin:0 auto;padding:var(--spacing-xl) 0}.setup-title{font-size:2rem;font-weight:700;color:var(--text-primary);text-align:center;margin-bottom:var(--spacing-sm)}.setup-subtitle{text-align:center;color:var(--text-secondary);margin-bottom:var(--spacing-xl)}.profile-form{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-xl);box-shadow:var(--shadow-md)}.form-group{margin-bottom:var(--spacing-lg)}.form-group label{display:block;font-weight:600;color:var(--text-primary);margin-bottom:var(--spacing-sm)}.form-group input,.form-group select{width:100%;padding:12px 16px;border:2px solid var(--border);border-radius:var(--radius-md);font-size:1rem;font-family:inherit;transition:all .3s ease}.form-group input:focus,.form-group select:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(255,107,157,0.1)}.form-row{display:grid;grid-template-columns:1fr 1fr;gap:var(--spacing-md)}.photo-upload{text-align:center}.photo-preview{width:150px;height:150px;margin:0 auto;border-radius:var(--radius-full);border:3px dashed var(--border);cursor:pointer;transition:all .3s ease;overflow:hidden;display:flex;align-items:center;justify-content:center}.photo-preview:hover{border-color:var(--primary);transform:scale(1.05)}.photo-placeholder{text-align:center}.photo-icon{font-size:48px;display:block;margin-bottom:var(--spacing-sm)}.photo-text{color:var(--text-secondary);font-size:.9rem}.profile-photo{width:100%;height:100%;object-fit:cover}.btn-setup-submit{width:100%;padding:16px;background:var(--gradient-primary);color:var(--white);border:none;border-radius:var(--radius-full);font-size:1.1rem;font-weight:600;cursor:pointer;box-shadow:var(--shadow-md);transition:all .3s ease;margin-top:var(--spacing-lg)}.btn-setup-submit:hover{box-shadow:var(--shadow-lg);transform:translateY(-2px)}.welcome-screen{position:fixed;top:0;left:0;width:100%;height:100vh;background:var(--gradient-hero);display:flex;align-items:center;justify-content:center;z-index:9999;animation:fadeIn .6s ease;will-change:opacity;contain:layout style paint}.welcome-content{text-align:center;color:var(--white);padding:var(--spacing-xl)}.welcome-logo{margin-bottom:var(--spacing-xl)}.logo-heart{font-size:80px;animation:heartbeat 1.5s ease infinite}@keyframes heartbeat{0%,100%{transform:scale(1)}50%{transform:scale(1.1)}}.welcome-content h1{font-family:'Playfair Display',serif;font-size:2.5rem;font-weight:700;margin-bottom:var(--spacing-sm);text-shadow:0 2px 8px rgba(0,0,0,0.2)}.welcome-content p{font-size:1.1rem;opacity:.95;margin-bottom:var(--spacing-xl)}.btn-start-journey{background:var(--white);color:var(--primary);border:none;padding:16px 48px;font-size:1.1rem;font-weight:600;border-radius:var(--radius-full);cursor:pointer;box-shadow:0 8px 24px rgba(0,0,0,0.2);transition:all .3s cubic-bezier(0.4,0,0.2,1)}.btn-start-journey:hover{transform:translateY(-2px);box-shadow:0 12px 32px rgba(0,0,0,0.25)}.btn-start-journey:active{transform:translateY(0)}.app-container{max-width:480px;margin:0 auto;background:var(--bg-light);min-height:100vh;min-height:-webkit-fill-available;position:relative;padding-bottom:80px;padding-bottom:calc(80px+env(safe-area-inset-bottom,0px));overflow-x:hidden}.top-bar{background:var(--gradient-hero);padding:var(--spacing-md) var(--spacing-lg);display:flex;justify-content:space-between;align-items:center;color:var(--white);box-shadow:var(--shadow-md)}.user-info{display:flex;align-items:center;gap:var(--spacing-md)}.avatar{width:48px;height:48px;border-radius:var(--radius-full);background:rgba(255,255,255,0.2);display:flex;align-items:center;justify-content:center;font-size:24px;border:2px solid rgba(255,255,255,0.3)}.user-text{display:flex;flex-direction:column}.greeting{font-weight:600;font-size:1rem}.streak{font-size:.85rem;opacity:.9}.icon-btn{background:rgba(255,255,255,0.2);border:none;width:40px;height:40px;border-radius:var(--radius-full);display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:20px;transition:all .3s ease}.icon-btn:hover{background:rgba(255,255,255,0.3);transform:scale(1.05)}.main-view{padding:var(--spacing-lg)}.view{display:none;animation:slideIn .3s ease}.view.active{display:block}@keyframes slideIn{from{opacity:0;transform:translateX(20px)}to{opacity:1;transform:translateX(0)}}.hero-section{margin-bottom:var(--spacing-xl)}.page-title{font-family:'Playfair Display',serif;font-size:2rem;font-weight:700;color:var(--text-primary);margin-bottom:var(--spacing-lg)}.daily-progress{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);box-shadow:var(--shadow-md)}.progress-circle{position:relative;width:120px;height:120px;margin:0 auto var(--spacing-lg)}.progress-circle svg{width:100%;height:100%;transform:rotate(-90deg)}.progress-bg{fill:none;stroke:var(--border);stroke-width:8}.progress-fill{fill:none;stroke:url(#progressGradient);stroke-width:8;stroke-linecap:round;stroke-dasharray:339.292;stroke-dashoffset:calc(339.292 - (339.292 * var(--progress)) / 100);transition:stroke-dashoffset .5s ease}.progress-text{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}.progress-value{display:block;font-size:2rem;font-weight:700;background:var(--gradient-primary);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.progress-label{font-size:.85rem;color:var(--text-secondary)}.today-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--spacing-md)}.stat{text-align:center;padding:var(--spacing-md);background:var(--gradient-soft);border-radius:var(--radius-md)}.stat-icon{font-size:24px;display:block;margin-bottom:var(--spacing-xs)}.stat-value{font-size:1.25rem;font-weight:700;color:var(--primary)}.stat-label{font-size:.75rem;color:var(--text-secondary)}.quick-actions{margin-bottom:var(--spacing-xl)}.section-title{font-size:1.25rem;font-weight:600;margin-bottom:var(--spacing-md);color:var(--text-primary)}.action-cards{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--spacing-md)}.action-card{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);text-align:center;box-shadow:var(--shadow-sm);cursor:pointer;transition:all .3s cubic-bezier(0.4,0,0.2,1);border:2px solid transparent;will-change:transform;transform:translateZ(0)}.action-card:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg);border-color:var(--primary)}.action-card:active{transform:translateY(-2px)}.action-icon{font-size:48px;margin-bottom:var(--spacing-sm);filter:drop-shadow(0 2px 4px rgba(0,0,0,0.1))}.action-card h4{font-size:1rem;font-weight:600;color:var(--text-primary);margin-bottom:var(--spacing-xs)}.action-card p{font-size:.85rem;color:var(--text-secondary)}.motivation-card{background:var(--gradient-hero);border-radius:var(--radius-lg);padding:var(--spacing-lg);text-align:center;box-shadow:var(--shadow-md);margin-bottom:var(--spacing-lg)}.motivation-icon{font-size:32px;margin-bottom:var(--spacing-sm)}.motivation-text{color:var(--white);font-size:1rem;line-height:1.6;font-weight:500}.category-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--spacing-md)}.category-card{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);text-align:center;box-shadow:var(--shadow-sm);cursor:pointer;transition:all .3s cubic-bezier(0.4,0,0.2,1);position:relative;overflow:hidden;will-change:transform;transform:translateZ(0);contain:layout style}.category-card::before{content:'';position:absolute;top:0;left:0;right:0;height:4px;background:var(--gradient-primary);transform:scaleX(0);transition:transform .3s ease}.category-card:hover::before{transform:scaleX(1)}.category-card:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}.category-image{font-size:48px;margin-bottom:var(--spacing-sm)}.category-card h3{font-size:1.1rem;font-weight:600;color:var(--text-primary);margin-bottom:var(--spacing-xs)}.category-card p{font-size:.85rem;color:var(--text-secondary);margin-bottom:var(--spacing-sm)}.category-badge{display:inline-block;background:var(--gradient-soft);color:var(--primary);padding:4px 12px;border-radius:var(--radius-full);font-size:.75rem;font-weight:500}.view-header{margin-bottom:var(--spacing-lg)}.btn-back{background:var(--white);border:none;padding:var(--spacing-sm) var(--spacing-md);border-radius:var(--radius-full);font-weight:500;color:var(--text-primary);cursor:pointer;box-shadow:var(--shadow-sm);margin-bottom:var(--spacing-md);transition:all .3s ease}.btn-back:hover{box-shadow:var(--shadow-md);transform:translateX(-2px)}.view-title{font-family:'Playfair Display',serif;font-size:1.75rem;font-weight:700;color:var(--text-primary)}.exercises-container{display:flex;flex-direction:column;gap:var(--spacing-md)}.exercise-card{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);box-shadow:var(--shadow-sm);cursor:pointer;transition:all .3s ease;display:flex;align-items:center;gap:var(--spacing-md)}.exercise-card:hover{box-shadow:var(--shadow-md);transform:translateX(4px)}.section-header{margin:var(--spacing-xl) 0 var(--spacing-md) 0;padding:var(--spacing-sm) var(--spacing-md);background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border-radius:var(--radius-md);box-shadow:var(--shadow-sm)}.section-header:first-child{margin-top:0}.section-header h3{color:var(--white);font-size:1.1rem;font-weight:600;margin:0;text-align:center}.exercise-emoji{font-size:40px;flex-shrink:0}.exercise-info{flex:1}.exercise-name{font-size:1rem;font-weight:600;color:var(--text-primary);margin-bottom:var(--spacing-xs)}.exercise-details{font-size:.85rem;color:var(--text-secondary)}.exercise-arrow{font-size:20px;color:var(--primary)}.workout-header{background:var(--gradient-hero);padding:var(--spacing-lg);display:flex;justify-content:space-between;align-items:center;color:var(--white)}.btn-close-workout{background:rgba(255,255,255,0.2);border:none;width:40px;height:40px;border-radius:var(--radius-full);color:var(--white);font-size:24px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s ease}.btn-close-workout:hover{background:rgba(255,255,255,0.3)}.workout-timer{font-size:1.5rem;font-weight:700;font-family:'Courier New',monospace}.workout-content{padding:var(--spacing-xl) var(--spacing-lg)}.exercise-display{text-align:center}.exercise-name{font-size:1.5rem;font-weight:700;color:var(--text-primary);margin-bottom:var(--spacing-sm)}.exercise-count{font-size:.9rem;color:var(--text-secondary);margin-bottom:var(--spacing-lg)}.exercise-demo{margin:var(--spacing-xl) 0}.demo-placeholder{width:95%;max-width:700px;margin:0 auto;background:transparent;border-radius:16px;display:flex;align-items:center;justify-content:center;overflow:hidden;position:relative}.demo-icon{font-size:80px}.demo-video{width:100%;height:auto;max-height:75vh;object-fit:contain;border-radius:16px;box-shadow:0 4px 12px rgba(0,0,0,0.15);display:block}.exercise-instructions{margin-bottom:var(--spacing-lg)}.reps-info{font-size:1.1rem;font-weight:600;color:var(--primary);margin-bottom:var(--spacing-sm)}.rest-info{font-size:.9rem;color:var(--text-secondary)}.series-tracker{display:flex;justify-content:center;gap:var(--spacing-sm);margin-bottom:var(--spacing-xl)}.series-dot{width:12px;height:12px;border-radius:var(--radius-full);background:var(--border);transition:all .3s ease}.series-dot.completed{background:var(--primary);transform:scale(1.2)}.workout-controls{display:flex;gap:var(--spacing-md)}.btn-workout-action{flex:1;padding:16px;border:none;border-radius:var(--radius-full);font-size:1rem;font-weight:600;cursor:pointer;transition:all .3s ease}.btn-workout-action.primary{background:var(--gradient-primary);color:var(--white);box-shadow:var(--shadow-md)}.btn-workout-action.primary:hover{box-shadow:var(--shadow-lg);transform:translateY(-2px)}.btn-workout-action.secondary{background:var(--white);color:var(--text-primary);box-shadow:var(--shadow-sm)}.workout-progress-bar{position:fixed;bottom:80px;left:0;right:0;height:4px;background:var(--border);max-width:480px;margin:0 auto}.progress-bar-fill{height:100%;background:var(--gradient-primary);transition:width .3s ease}.wellness-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--spacing-md)}.wellness-card{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);text-align:center;box-shadow:var(--shadow-sm);cursor:pointer;transition:all .3s ease}.wellness-card:hover{transform:translateY(-4px);box-shadow:var(--shadow-lg)}.wellness-icon{font-size:48px;margin-bottom:var(--spacing-sm)}.wellness-card h3{font-size:1rem;font-weight:600;color:var(--text-primary);margin-bottom:var(--spacing-xs)}.wellness-card p{font-size:.85rem;color:var(--text-secondary);margin-bottom:var(--spacing-sm)}.duration{display:inline-block;background:var(--gradient-soft);color:var(--primary);padding:4px 12px;border-radius:var(--radius-full);font-size:.75rem;font-weight:500}.nutrition-summary{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);box-shadow:var(--shadow-md);margin-bottom:var(--spacing-lg)}.nutrition-summary h3{font-size:1.25rem;font-weight:600;margin-bottom:var(--spacing-lg);text-align:center}.macros-display{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--spacing-md);margin-bottom:var(--spacing-lg)}.macro-item{text-align:center}.macro-circle{width:80px;height:80px;border-radius:var(--radius-full);display:flex;align-items:center;justify-content:center;margin:0 auto var(--spacing-sm);font-weight:700;color:var(--white)}.macro-circle.carbs{background:linear-gradient(135deg,#FFB74D 0,#FF9800 100%)}.macro-circle.protein{background:linear-gradient(135deg,#4CAF50 0,#388E3C 100%)}.macro-circle.fat{background:linear-gradient(135deg,#9C27B0 0,#7B1FA2 100%)}.macro-label{font-size:.85rem;color:var(--text-secondary)}.calories-total{text-align:center;padding:var(--spacing-md);background:var(--gradient-soft);border-radius:var(--radius-md)}.calories-value{display:block;font-size:2rem;font-weight:700;background:var(--gradient-primary);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.calories-label{font-size:.9rem;color:var(--text-secondary)}.water-tracker{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);box-shadow:var(--shadow-md)}.water-tracker h3{font-size:1.25rem;font-weight:600;margin-bottom:var(--spacing-md);text-align:center}.water-glasses{display:grid;grid-template-columns:repeat(4,1fr);gap:var(--spacing-sm);margin-bottom:var(--spacing-md)}.glass{aspect-ratio:1;background:var(--border);border-radius:var(--radius-md);display:flex;align-items:center;justify-content:center;font-size:28px;cursor:pointer;transition:all .3s ease;opacity:.3}.glass.filled{background:linear-gradient(135deg,#64B5F6 0,#2196F3 100%);opacity:1;transform:scale(1.05)}.water-goal{text-align:center;color:var(--text-secondary);font-size:.9rem}.achievements-section,.stats-section{margin-bottom:var(--spacing-xl)}.achievements-section h3,.stats-section h3{font-size:1.25rem;font-weight:600;margin-bottom:var(--spacing-md)}.plan-summary{margin:var(--spacing-xl) 0}.plan-card{background:linear-gradient(135deg,#667eea 0,#764ba2 100%);border-radius:var(--radius-lg);padding:var(--spacing-lg);color:var(--white);box-shadow:var(--shadow-lg)}.plan-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--spacing-md)}.plan-header h3{font-size:1.3rem;font-weight:700}.btn-view-plan{background:rgba(255,255,255,0.2);color:var(--white);padding:8px 16px;border:1px solid rgba(255,255,255,0.3);border-radius:var(--radius-full);font-size:.9rem;font-weight:600;cursor:pointer;transition:all .3s ease}.btn-view-plan:hover{background:rgba(255,255,255,0.3);transform:scale(1.05)}.plan-quick-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--spacing-md);margin-bottom:var(--spacing-md)}.plan-stat{text-align:center}.plan-label{display:block;font-size:.85rem;opacity:.9;margin-bottom:4px}.plan-value{display:block;font-size:1.1rem;font-weight:700}.coach-message{background:rgba(255,255,255,0.15);padding:var(--spacing-md);border-radius:var(--radius-md);text-align:center;font-size:.95rem;border-left:4px solid rgba(255,255,255,0.5)}.plan-modal-content{max-width:600px;max-height:85vh;overflow-y:auto;width:95%;margin:auto}.profile-info{background:var(--bg-light);padding:var(--spacing-lg);border-radius:var(--radius-md);margin-bottom:var(--spacing-lg)}.profile-photo-container{display:flex;align-items:center;gap:var(--spacing-md);margin-bottom:var(--spacing-md)}.profile-photo-large{width:80px;height:80px;border-radius:var(--radius-full);object-fit:cover}.profile-basic-info{flex:1}.profile-name{font-size:1.5rem;font-weight:700;color:var(--text-primary);margin-bottom:4px}.profile-metrics{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--spacing-sm);margin-top:var(--spacing-md)}.metric-item{display:flex;justify-content:space-between;padding:8px;background:var(--white);border-radius:var(--radius-sm)}.metric-label{color:var(--text-secondary);font-size:.9rem}.metric-value{font-weight:600;color:var(--text-primary)}.plan-section{margin-bottom:var(--spacing-xl)}.plan-section h3{font-size:1.2rem;font-weight:700;color:var(--text-primary);margin-bottom:var(--spacing-md);padding-bottom:var(--spacing-sm);border-bottom:2px solid var(--border)}.nutrition-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--spacing-md);margin-bottom:var(--spacing-md)}.nutrition-item{background:var(--bg-light);padding:var(--spacing-md);border-radius:var(--radius-md);text-align:center}.nutrition-label{display:block;font-size:.9rem;color:var(--text-secondary);margin-bottom:4px}.nutrition-value{display:block;font-size:1.5rem;font-weight:700;color:var(--primary)}.nutrition-unit{font-size:.9rem;color:var(--text-secondary);margin-left:4px}.meal-plan{background:var(--bg-light);padding:var(--spacing-md);border-radius:var(--radius-md)}.meal-item{padding:var(--spacing-sm) 0;border-bottom:1px solid var(--border)}.meal-item:last-child{border-bottom:none}.meal-name{font-weight:600;color:var(--text-primary);margin-bottom:4px}.meal-description{font-size:.9rem;color:var(--text-secondary)}.workout-info,.timeline-info{background:var(--bg-light);padding:var(--spacing-md);border-radius:var(--radius-md)}.info-row{display:flex;justify-content:space-between;padding:var(--spacing-sm) 0;border-bottom:1px solid var(--border)}.info-row:last-child{border-bottom:none}.info-label{color:var(--text-secondary)}.info-value{font-weight:600;color:var(--text-primary)}.milestones{display:grid;gap:var(--spacing-sm);margin-top:var(--spacing-md)}.milestone-item{display:flex;align-items:center;gap:var(--spacing-md);padding:var(--spacing-sm);background:var(--bg-light);border-radius:var(--radius-md)}.milestone-check{width:30px;height:30px;border-radius:var(--radius-full);background:var(--success);color:var(--white);display:flex;align-items:center;justify-content:center;font-size:1.2rem}.tips-list{display:grid;gap:var(--spacing-sm)}.tip-item{background:var(--bg-light);padding:var(--spacing-md);border-radius:var(--radius-md);border-left:4px solid var(--primary)}.btn-edit-profile{width:100%;padding:16px;background:var(--gradient-primary);color:var(--white);border:none;border-radius:var(--radius-full);font-size:1.1rem;font-weight:600;cursor:pointer;box-shadow:var(--shadow-md);transition:all .3s ease;margin-top:var(--spacing-lg)}.btn-edit-profile:hover{box-shadow:var(--shadow-lg);transform:translateY(-2px)}.achievements-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--spacing-md)}.achievement-card{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-md);text-align:center;box-shadow:var(--shadow-sm)}.achievement-card{transition:all .3s ease}.achievement-card.locked{opacity:.4;filter:grayscale(1)}.achievement-card:not(.locked):hover{transform:translateY(-4px);box-shadow:var(--shadow-md)}.achievement-icon{font-size:40px;margin-bottom:var(--spacing-xs);display:block}.achievement-name{font-size:.75rem;font-weight:600;color:var(--text-primary)}@media (max-width:480px){.demo-placeholder{width:98%;max-width:100%}.demo-video{width:100%;height:auto;max-height:65vh;border-radius:12px}.achievements-grid{grid-template-columns:repeat(2,1fr);gap:var(--spacing-sm)}.form-row{grid-template-columns:1fr}.plan-quick-stats{grid-template-columns:1fr;gap:var(--spacing-sm)}.profile-metrics{grid-template-columns:1fr}.nutrition-grid{grid-template-columns:1fr}.achievement-card{padding:var(--spacing-sm)}.achievement-icon{font-size:32px}.achievement-name{font-size:.7rem}}.stats-cards{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--spacing-md)}.stat-card{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);text-align:center;box-shadow:var(--shadow-sm)}.stat-card .stat-icon{font-size:32px;margin-bottom:var(--spacing-sm)}.stat-number{font-size:1.75rem;font-weight:700;background:var(--gradient-primary);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}.stat-label{font-size:.85rem;color:var(--text-secondary)}.bottom-nav{position:fixed;bottom:0;left:0;right:0;max-width:480px;margin:0 auto;background:var(--white);box-shadow:0 -2px 16px rgba(0,0,0,0.1);display:grid;grid-template-columns:repeat(4,1fr);padding:var(--spacing-sm) 0;z-index:100}.nav-item{background:none;border:none;padding:var(--spacing-sm);display:flex;flex-direction:column;align-items:center;gap:4px;cursor:pointer;color:var(--text-secondary);transition:all .3s ease}.nav-item.active{color:var(--primary)}.nav-icon{font-size:24px;transition:transform .3s ease}.nav-item.active .nav-icon{transform:scale(1.1)}.nav-label{font-size:.7rem;font-weight:500}.modal{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);z-index:1000;align-items:center;justify-content:center;animation:fadeIn .3s ease}.modal.active{display:flex}.modal-content{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-xl);max-width:90%;max-height:90vh;width:360px;overflow-y:auto;box-shadow:var(--shadow-xl);text-align:center;animation:scaleIn .3s cubic-bezier(0.4,0,0.2,1)}@keyframes scaleIn{from{opacity:0;transform:scale(0.9)}to{opacity:1;transform:scale(1)}}.celebration-confetti{font-size:64px;margin-bottom:var(--spacing-md);animation:bounce .6s ease}@keyframes bounce{0%,100%{transform:translateY(0)}50%{transform:translateY(-20px)}}.modal-title{font-size:1.75rem;font-weight:700;color:var(--text-primary);margin-bottom:var(--spacing-md)}.modal-message{font-size:1rem;color:var(--text-secondary);margin-bottom:var(--spacing-lg)}.workout-summary{display:flex;justify-content:center;gap:var(--spacing-lg);margin-bottom:var(--spacing-lg)}.summary-stat{display:flex;align-items:center;gap:var(--spacing-sm)}.summary-icon{font-size:24px}.summary-value{font-weight:600;color:var(--primary)}.btn-modal-primary{background:var(--gradient-primary);color:var(--white);border:none;padding:16px 48px;border-radius:var(--radius-full);font-size:1rem;font-weight:600;cursor:pointer;box-shadow:var(--shadow-md);transition:all .3s ease}.btn-modal-primary:hover{box-shadow:var(--shadow-lg);transform:translateY(-2px)}.weight-tracking-section{margin-bottom:var(--spacing-xl)}.weight-tracking-section h3{font-size:1.25rem;font-weight:600;margin-bottom:var(--spacing-md)}.weight-card{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);box-shadow:var(--shadow-md)}.weight-current{text-align:center;margin-bottom:var(--spacing-lg)}.weight-label{font-size:.9rem;color:var(--text-secondary);margin-bottom:var(--spacing-xs)}.weight-value{font-size:3rem;font-weight:700;background:var(--gradient-primary);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;margin-bottom:var(--spacing-md)}.btn-update-weight{background:var(--gradient-primary);color:var(--white);border:none;padding:12px 32px;border-radius:var(--radius-full);font-weight:600;cursor:pointer;box-shadow:var(--shadow-sm);transition:all .3s ease}.btn-update-weight:hover{box-shadow:var(--shadow-md);transform:translateY(-2px)}.weight-stats{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--spacing-md);margin-bottom:var(--spacing-lg)}.weight-stat{text-align:center;padding:var(--spacing-md);background:var(--bg-light);border-radius:var(--radius-md)}.weight-stat.success{background:linear-gradient(135deg,#E8F5E9 0,#C8E6C9 100%)}.weight-stat-label{font-size:.75rem;color:var(--text-secondary);margin-bottom:var(--spacing-xs)}.weight-stat-value{font-size:1.1rem;font-weight:700;color:var(--text-primary)}.weight-progress-bar{width:100%;height:8px;background:var(--border);border-radius:var(--radius-full);overflow:hidden;margin-bottom:var(--spacing-lg)}.weight-progress-fill{height:100%;background:var(--gradient-primary);transition:width .5s ease}.weight-chart-mini{height:100px;display:flex;align-items:flex-end;gap:4px;padding:var(--spacing-md) 0}.weight-chart-bar{flex:1;background:var(--gradient-primary);border-radius:4px 4px 0 0;min-height:20px;transition:height .3s ease}.detailed-stats-section{margin-bottom:var(--spacing-xl)}.detailed-stats-section h3{font-size:1.25rem;font-weight:600;margin-bottom:var(--spacing-md)}.stats-grid{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--spacing-md)}.stat-detail-card{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);box-shadow:var(--shadow-sm);display:flex;gap:var(--spacing-md)}.stat-detail-icon{font-size:36px;flex-shrink:0}.stat-detail-content{flex:1}.stat-detail-number{font-size:1.75rem;font-weight:700;background:var(--gradient-primary);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text;line-height:1;margin-bottom:var(--spacing-xs)}.stat-detail-label{font-size:.85rem;color:var(--text-primary);font-weight:500;margin-bottom:4px}.stat-detail-sublabel{font-size:.75rem;color:var(--text-secondary)}.activity-chart-section{margin-bottom:var(--spacing-xl)}.activity-chart-section h3{font-size:1.25rem;font-weight:600;margin-bottom:var(--spacing-md)}.weekly-chart{background:var(--white);border-radius:var(--radius-lg);padding:var(--spacing-lg);box-shadow:var(--shadow-sm)}.chart-bars{display:flex;align-items:flex-end;justify-content:space-around;gap:var(--spacing-sm);height:150px}.chart-day{flex:1;display:flex;flex-direction:column;align-items:center;gap:var(--spacing-xs)}.chart-bar{width:100%;background:var(--gradient-primary);border-radius:4px 4px 0 0;min-height:4px;transition:height .3s ease}.chart-label{font-size:.7rem;color:var(--text-secondary);font-weight:500}.records-section{margin-bottom:var(--spacing-xl)}.records-section h3{font-size:1.25rem;font-weight:600;margin-bottom:var(--spacing-md)}.records-list{display:flex;flex-direction:column;gap:var(--spacing-sm)}.record-item{background:var(--white);border-radius:var(--radius-md);padding:var(--spacing-md);box-shadow:var(--shadow-sm);display:flex;align-items:center;gap:var(--spacing-md)}.record-icon{font-size:28px}.record-content{flex:1}.record-label{font-size:.85rem;color:var(--text-secondary);margin-bottom:2px}.record-value{font-size:1rem;font-weight:600;color:var(--text-primary)}.weight-input-group{margin-bottom:var(--spacing-md)}.weight-input-group label{display:block;font-size:.9rem;font-weight:500;color:var(--text-primary);margin-bottom:var(--spacing-xs)}.weight-input-group input{width:100%;padding:12px 16px;border:2px solid var(--border);border-radius:var(--radius-md);font-size:1rem;font-family:inherit;transition:all .3s ease}.weight-input-group input:focus{outline:none;border-color:var(--primary);box-shadow:0 0 0 3px rgba(255,107,157,0.1)}.modal-actions{display:flex;gap:var(--spacing-md);margin-top:var(--spacing-lg)}.btn-modal-secondary{flex:1;background:var(--bg-light);color:var(--text-primary);border:2px solid var(--border);padding:12px 24px;border-radius:var(--radius-full);font-size:1rem;font-weight:600;cursor:pointer;transition:all .3s ease}.btn-modal-secondary:hover{background:var(--border)}.settings-fab{position:fixed;bottom:100px;right:20px;z-index:99;max-width:480px;margin:0 auto}.fab-settings{width:56px;height:56px;border-radius:var(--radius-full);background:var(--gradient-primary);border:none;box-shadow:var(--shadow-lg);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .3s cubic-bezier(0.4,0,0.2,1)}.fab-settings:hover{transform:scale(1.1);box-shadow:var(--shadow-xl)}.fab-settings:active{transform:scale(0.95)}.fab-icon{font-size:24px}.video-loading{position:relative}.video-loader{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:10}.spinner{width:40px;height:40px;border:4px solid rgba(255,255,255,0.3);border-top-color:var(--primary);border-radius:var(--radius-full);animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.video-error{opacity:.5}.video-error::after{content:'⚠️ Error loading video';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:var(--white);background:rgba(0,0,0,0.7);padding:var(--spacing-sm) var(--spacing-md);border-radius:var(--radius-md);font-size:.85rem;z-index:10}@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes pulse{0%,100%{transform:scale(1)}50%{transform:scale(1.05)}}.pulse{animation:pulse 1s ease infinite}@media (max-width:768px){.view{padding:var(--spacing-md)}.main-view{padding:var(--spacing-md)}.top-bar{padding:var(--spacing-sm) var(--spacing-md)}.user-text .greeting{font-size:.9rem}.user-text .streak{font-size:.75rem}body{overflow-x:hidden}.app-container{overflow-x:hidden;max-width:100vw}.category-grid{grid-template-columns:repeat(2,1fr);gap:var(--spacing-sm)}.action-cards{grid-template-columns:repeat(2,1fr);gap:var(--spacing-sm)}.wellness-grid{grid-template-columns:repeat(2,1fr);gap:var(--spacing-sm)}.stats-grid{grid-template-columns:1fr;gap:var(--spacing-sm)}.today-stats{flex-direction:column;gap:var(--spacing-sm)}.category-card,.action-card{padding:var(--spacing-md)}.category-image,.action-icon{font-size:2rem}.page-title{font-size:1.5rem}.section-title{font-size:1.1rem}.modal-content{width:95%;max-width:none;margin:var(--spacing-md)}.weight-stats{grid-template-columns:1fr;gap:var(--spacing-sm)}.workout-header{padding:var(--spacing-md)}.demo-area{padding:var(--spacing-lg)}.demo-icon{font-size:4rem}.btn-back,.btn-primary,.btn-secondary{padding:12px 20px;font-size:.9rem}.bottom-nav{padding:var(--spacing-sm) 0}.nav-item{min-width:60px}.nav-icon{font-size:22px}.nav-label{font-size:.7rem}.settings-fab{bottom:80px;right:15px}.fab-settings{width:50px;height:50px}}@media (max-width:480px){.action-cards,.category-grid,.wellness-grid{grid-template-columns:1fr}.progress-circle{width:100px;height:100px}.progress-circle svg{width:100px;height:100px}.progress-value{font-size:1.5rem}.exercise-card{padding:var(--spacing-sm)}.water-glasses{gap:var(--spacing-xs);grid-template-columns:repeat(4,1fr)}.glass{font-size:1.2rem}.modal-content{width:90%;padding:var(--spacing-lg);margin:var(--spacing-md);max-height:85vh}.plan-modal-content{width:95%;max-width:none;max-height:85vh}.demo-placeholder{width:95%;max-width:100%}.demo-video{max-height:50vh}.demo-icon{font-size:60px}.bottom-nav{padding-bottom:env(safe-area-inset-bottom,var(--spacing-sm))}}@media (max-width:360px){.page-title{font-size:1.3rem}.hero-section{padding:var(--spacing-md) 0}.stat-detail-number{font-size:1.5rem}.weight-value{font-size:2.5rem}.btn-primary,.btn-secondary{padding:10px 16px;font-size:.85rem}}@media (min-width:769px){.app-container{max-width:768px;margin:0 auto}.category-grid{grid-template-columns:repeat(3,1fr)}.action-cards{grid-template-columns:repeat(3,1fr)}}svg defs{position:absolute;width:0;height:0}
dist/styles.min.css.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a95af5e5c6e813fea4624f63042d49d38e1b5fed8dade2001b187f0850d339a2
3
+ size 5741
dist/sw.min.js ADDED
@@ -0,0 +1 @@
 
 
1
+ const VERSION='3.1.0'; const CACHE_NAME=`fitness-app-${VERSION}`; const STATIC_CACHE=`static-${VERSION}`; const DYNAMIC_CACHE=`dynamic-${VERSION}`; const VIDEO_CACHE=`video-${VERSION}`; const MAX_VIDEO_CACHE=5; const STATIC_ASSETS=['/','/index.html','/app.js','/styles.css','/manifest.json','/icons/icon-192x192.svg','/icons/icon-512x512.svg'];self.addEventListener('install',(event)=>{event.waitUntil(caches.open(STATIC_CACHE).then(cache=>cache.addAll(STATIC_ASSETS)).then(()=>self.skipWaiting()));});self.addEventListener('activate',(event)=>{event.waitUntil(caches.keys().then(keys=>{ return Promise.all(keys.filter(key=>key!==STATIC_CACHE&&key!==DYNAMIC_CACHE&&key!==VIDEO_CACHE).map(key=>caches.delete(key)));}).then(()=>self.clients.claim()));});self.addEventListener('fetch',(event)=>{const{request}=event; const url= new URL(request.url);if(url.origin!==location.origin){return;}if(request.url.includes('/videos/')){event.respondWith(caches.open(VIDEO_CACHE).then(cache=>{ return cache.match(request).then(cachedResponse=>{if(cachedResponse){ return cachedResponse;} return fetch(request).then(networkResponse=>{if(networkResponse.status===200){cache.put(request,networkResponse.clone());cache.keys().then(keys=>{if(keys.length>MAX_VIDEO_CACHE){cache.delete(keys[0]);}});} return networkResponse;});});}).catch(()=>{ return caches.match(request);}));return;}if(request.url.includes('/songs/')){event.respondWith(caches.match(request).then(response=>response||fetch(request).then(fetchResponse=>{ return caches.open(DYNAMIC_CACHE).then(cache=>{cache.put(request,fetchResponse.clone()); return fetchResponse;});})));return;}event.respondWith(caches.match(request).then(response=>{if(response) return response; return fetch(request).then(fetchResponse=>{if(fetchResponse.status===200){ const responseClone=fetchResponse.clone();caches.open(DYNAMIC_CACHE).then(cache=>{cache.put(request,responseClone);});} return fetchResponse;});}).catch(()=>{if(request.destination==='document'){ return caches.match('/index.html');}}));});
dist/sw.min.js.gz ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4aff62abda79636578db6bdc20a071a55b560bf9d13ec15302eab71f793c0e4d
3
+ size 730
docker-compose.yml ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ keto-planner:
5
+ build: .
6
+ ports:
7
+ - "7860:7860"
8
+ environment:
9
+ - NODE_ENV=production
10
+ - PORT=7860
11
+ restart: unless-stopped
12
+ healthcheck:
13
+ test: ["CMD", "node", "-e", "require('http').get('http://localhost:7860/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"]
14
+ interval: 30s
15
+ timeout: 10s
16
+ retries: 3
17
+ start_period: 40s
generate-icons.html ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Icon Generator</title>
5
+ </head>
6
+ <body>
7
+ <canvas id="canvas" width="512" height="512" style="display: none;"></canvas>
8
+ <script>
9
+ // Generate PWA icons
10
+ function generateIcon(size) {
11
+ const canvas = document.getElementById('canvas');
12
+ const ctx = canvas.getContext('2d');
13
+
14
+ canvas.width = size;
15
+ canvas.height = size;
16
+
17
+ // Clear canvas
18
+ ctx.clearRect(0, 0, size, size);
19
+
20
+ // Create gradient background
21
+ const gradient = ctx.createLinearGradient(0, 0, size, size);
22
+ gradient.addColorStop(0, '#1a0d2e');
23
+ gradient.addColorStop(0.5, '#6b46c1');
24
+ gradient.addColorStop(1, '#d946ef');
25
+
26
+ ctx.fillStyle = gradient;
27
+ ctx.fillRect(0, 0, size, size);
28
+
29
+ // Add rounded corners
30
+ ctx.globalCompositeOperation = 'destination-in';
31
+ ctx.beginPath();
32
+ ctx.roundRect(0, 0, size, size, size * 0.1);
33
+ ctx.fill();
34
+ ctx.globalCompositeOperation = 'source-over';
35
+
36
+ // Add fire emoji
37
+ ctx.font = `${size * 0.4}px Arial`;
38
+ ctx.textAlign = 'center';
39
+ ctx.textBaseline = 'middle';
40
+ ctx.fillStyle = '#ffffff';
41
+ ctx.fillText('🔥', size / 2, size / 2);
42
+
43
+ // Convert to blob and download
44
+ canvas.toBlob((blob) => {
45
+ const url = URL.createObjectURL(blob);
46
+ const a = document.createElement('a');
47
+ a.href = url;
48
+ a.download = `icon-${size}x${size}.png`;
49
+ a.click();
50
+ URL.revokeObjectURL(url);
51
+ });
52
+ }
53
+
54
+ // Generate all required icon sizes
55
+ const sizes = [72, 96, 128, 144, 152, 192, 384, 512];
56
+ sizes.forEach((size, index) => {
57
+ setTimeout(() => generateIcon(size), index * 100);
58
+ });
59
+ </script>
60
+ </body>
61
+ </html>
package-lock.json ADDED
@@ -0,0 +1,1303 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "30-day-keto-planner",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "30-day-keto-planner",
9
+ "version": "1.0.0",
10
+ "license": "MIT",
11
+ "dependencies": {
12
+ "compression": "^1.7.4",
13
+ "cors": "^2.8.5",
14
+ "express": "^4.18.2",
15
+ "helmet": "^7.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "nodemon": "^3.0.1"
19
+ },
20
+ "engines": {
21
+ "node": ">=18.0.0"
22
+ }
23
+ },
24
+ "node_modules/accepts": {
25
+ "version": "1.3.8",
26
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
27
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
28
+ "license": "MIT",
29
+ "dependencies": {
30
+ "mime-types": "~2.1.34",
31
+ "negotiator": "0.6.3"
32
+ },
33
+ "engines": {
34
+ "node": ">= 0.6"
35
+ }
36
+ },
37
+ "node_modules/accepts/node_modules/negotiator": {
38
+ "version": "0.6.3",
39
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
40
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
41
+ "license": "MIT",
42
+ "engines": {
43
+ "node": ">= 0.6"
44
+ }
45
+ },
46
+ "node_modules/anymatch": {
47
+ "version": "3.1.3",
48
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
49
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
50
+ "dev": true,
51
+ "license": "ISC",
52
+ "dependencies": {
53
+ "normalize-path": "^3.0.0",
54
+ "picomatch": "^2.0.4"
55
+ },
56
+ "engines": {
57
+ "node": ">= 8"
58
+ }
59
+ },
60
+ "node_modules/array-flatten": {
61
+ "version": "1.1.1",
62
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
63
+ "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
64
+ "license": "MIT"
65
+ },
66
+ "node_modules/balanced-match": {
67
+ "version": "1.0.2",
68
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
69
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
70
+ "dev": true,
71
+ "license": "MIT"
72
+ },
73
+ "node_modules/binary-extensions": {
74
+ "version": "2.3.0",
75
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
76
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
77
+ "dev": true,
78
+ "license": "MIT",
79
+ "engines": {
80
+ "node": ">=8"
81
+ },
82
+ "funding": {
83
+ "url": "https://github.com/sponsors/sindresorhus"
84
+ }
85
+ },
86
+ "node_modules/body-parser": {
87
+ "version": "1.20.3",
88
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
89
+ "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
90
+ "license": "MIT",
91
+ "dependencies": {
92
+ "bytes": "3.1.2",
93
+ "content-type": "~1.0.5",
94
+ "debug": "2.6.9",
95
+ "depd": "2.0.0",
96
+ "destroy": "1.2.0",
97
+ "http-errors": "2.0.0",
98
+ "iconv-lite": "0.4.24",
99
+ "on-finished": "2.4.1",
100
+ "qs": "6.13.0",
101
+ "raw-body": "2.5.2",
102
+ "type-is": "~1.6.18",
103
+ "unpipe": "1.0.0"
104
+ },
105
+ "engines": {
106
+ "node": ">= 0.8",
107
+ "npm": "1.2.8000 || >= 1.4.16"
108
+ }
109
+ },
110
+ "node_modules/brace-expansion": {
111
+ "version": "1.1.12",
112
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
113
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
114
+ "dev": true,
115
+ "license": "MIT",
116
+ "dependencies": {
117
+ "balanced-match": "^1.0.0",
118
+ "concat-map": "0.0.1"
119
+ }
120
+ },
121
+ "node_modules/braces": {
122
+ "version": "3.0.3",
123
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
124
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
125
+ "dev": true,
126
+ "license": "MIT",
127
+ "dependencies": {
128
+ "fill-range": "^7.1.1"
129
+ },
130
+ "engines": {
131
+ "node": ">=8"
132
+ }
133
+ },
134
+ "node_modules/bytes": {
135
+ "version": "3.1.2",
136
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
137
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
138
+ "license": "MIT",
139
+ "engines": {
140
+ "node": ">= 0.8"
141
+ }
142
+ },
143
+ "node_modules/call-bind-apply-helpers": {
144
+ "version": "1.0.2",
145
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
146
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
147
+ "license": "MIT",
148
+ "dependencies": {
149
+ "es-errors": "^1.3.0",
150
+ "function-bind": "^1.1.2"
151
+ },
152
+ "engines": {
153
+ "node": ">= 0.4"
154
+ }
155
+ },
156
+ "node_modules/call-bound": {
157
+ "version": "1.0.4",
158
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
159
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
160
+ "license": "MIT",
161
+ "dependencies": {
162
+ "call-bind-apply-helpers": "^1.0.2",
163
+ "get-intrinsic": "^1.3.0"
164
+ },
165
+ "engines": {
166
+ "node": ">= 0.4"
167
+ },
168
+ "funding": {
169
+ "url": "https://github.com/sponsors/ljharb"
170
+ }
171
+ },
172
+ "node_modules/chokidar": {
173
+ "version": "3.6.0",
174
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
175
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
176
+ "dev": true,
177
+ "license": "MIT",
178
+ "dependencies": {
179
+ "anymatch": "~3.1.2",
180
+ "braces": "~3.0.2",
181
+ "glob-parent": "~5.1.2",
182
+ "is-binary-path": "~2.1.0",
183
+ "is-glob": "~4.0.1",
184
+ "normalize-path": "~3.0.0",
185
+ "readdirp": "~3.6.0"
186
+ },
187
+ "engines": {
188
+ "node": ">= 8.10.0"
189
+ },
190
+ "funding": {
191
+ "url": "https://paulmillr.com/funding/"
192
+ },
193
+ "optionalDependencies": {
194
+ "fsevents": "~2.3.2"
195
+ }
196
+ },
197
+ "node_modules/compressible": {
198
+ "version": "2.0.18",
199
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
200
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
201
+ "license": "MIT",
202
+ "dependencies": {
203
+ "mime-db": ">= 1.43.0 < 2"
204
+ },
205
+ "engines": {
206
+ "node": ">= 0.6"
207
+ }
208
+ },
209
+ "node_modules/compression": {
210
+ "version": "1.8.1",
211
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz",
212
+ "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==",
213
+ "license": "MIT",
214
+ "dependencies": {
215
+ "bytes": "3.1.2",
216
+ "compressible": "~2.0.18",
217
+ "debug": "2.6.9",
218
+ "negotiator": "~0.6.4",
219
+ "on-headers": "~1.1.0",
220
+ "safe-buffer": "5.2.1",
221
+ "vary": "~1.1.2"
222
+ },
223
+ "engines": {
224
+ "node": ">= 0.8.0"
225
+ }
226
+ },
227
+ "node_modules/concat-map": {
228
+ "version": "0.0.1",
229
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
230
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
231
+ "dev": true,
232
+ "license": "MIT"
233
+ },
234
+ "node_modules/content-disposition": {
235
+ "version": "0.5.4",
236
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
237
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
238
+ "license": "MIT",
239
+ "dependencies": {
240
+ "safe-buffer": "5.2.1"
241
+ },
242
+ "engines": {
243
+ "node": ">= 0.6"
244
+ }
245
+ },
246
+ "node_modules/content-type": {
247
+ "version": "1.0.5",
248
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
249
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
250
+ "license": "MIT",
251
+ "engines": {
252
+ "node": ">= 0.6"
253
+ }
254
+ },
255
+ "node_modules/cookie": {
256
+ "version": "0.7.1",
257
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
258
+ "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
259
+ "license": "MIT",
260
+ "engines": {
261
+ "node": ">= 0.6"
262
+ }
263
+ },
264
+ "node_modules/cookie-signature": {
265
+ "version": "1.0.6",
266
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
267
+ "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
268
+ "license": "MIT"
269
+ },
270
+ "node_modules/cors": {
271
+ "version": "2.8.5",
272
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
273
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
274
+ "license": "MIT",
275
+ "dependencies": {
276
+ "object-assign": "^4",
277
+ "vary": "^1"
278
+ },
279
+ "engines": {
280
+ "node": ">= 0.10"
281
+ }
282
+ },
283
+ "node_modules/debug": {
284
+ "version": "2.6.9",
285
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
286
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
287
+ "license": "MIT",
288
+ "dependencies": {
289
+ "ms": "2.0.0"
290
+ }
291
+ },
292
+ "node_modules/depd": {
293
+ "version": "2.0.0",
294
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
295
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
296
+ "license": "MIT",
297
+ "engines": {
298
+ "node": ">= 0.8"
299
+ }
300
+ },
301
+ "node_modules/destroy": {
302
+ "version": "1.2.0",
303
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
304
+ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
305
+ "license": "MIT",
306
+ "engines": {
307
+ "node": ">= 0.8",
308
+ "npm": "1.2.8000 || >= 1.4.16"
309
+ }
310
+ },
311
+ "node_modules/dunder-proto": {
312
+ "version": "1.0.1",
313
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
314
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
315
+ "license": "MIT",
316
+ "dependencies": {
317
+ "call-bind-apply-helpers": "^1.0.1",
318
+ "es-errors": "^1.3.0",
319
+ "gopd": "^1.2.0"
320
+ },
321
+ "engines": {
322
+ "node": ">= 0.4"
323
+ }
324
+ },
325
+ "node_modules/ee-first": {
326
+ "version": "1.1.1",
327
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
328
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
329
+ "license": "MIT"
330
+ },
331
+ "node_modules/encodeurl": {
332
+ "version": "2.0.0",
333
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
334
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
335
+ "license": "MIT",
336
+ "engines": {
337
+ "node": ">= 0.8"
338
+ }
339
+ },
340
+ "node_modules/es-define-property": {
341
+ "version": "1.0.1",
342
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
343
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
344
+ "license": "MIT",
345
+ "engines": {
346
+ "node": ">= 0.4"
347
+ }
348
+ },
349
+ "node_modules/es-errors": {
350
+ "version": "1.3.0",
351
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
352
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
353
+ "license": "MIT",
354
+ "engines": {
355
+ "node": ">= 0.4"
356
+ }
357
+ },
358
+ "node_modules/es-object-atoms": {
359
+ "version": "1.1.1",
360
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
361
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
362
+ "license": "MIT",
363
+ "dependencies": {
364
+ "es-errors": "^1.3.0"
365
+ },
366
+ "engines": {
367
+ "node": ">= 0.4"
368
+ }
369
+ },
370
+ "node_modules/escape-html": {
371
+ "version": "1.0.3",
372
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
373
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
374
+ "license": "MIT"
375
+ },
376
+ "node_modules/etag": {
377
+ "version": "1.8.1",
378
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
379
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
380
+ "license": "MIT",
381
+ "engines": {
382
+ "node": ">= 0.6"
383
+ }
384
+ },
385
+ "node_modules/express": {
386
+ "version": "4.21.2",
387
+ "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
388
+ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
389
+ "license": "MIT",
390
+ "dependencies": {
391
+ "accepts": "~1.3.8",
392
+ "array-flatten": "1.1.1",
393
+ "body-parser": "1.20.3",
394
+ "content-disposition": "0.5.4",
395
+ "content-type": "~1.0.4",
396
+ "cookie": "0.7.1",
397
+ "cookie-signature": "1.0.6",
398
+ "debug": "2.6.9",
399
+ "depd": "2.0.0",
400
+ "encodeurl": "~2.0.0",
401
+ "escape-html": "~1.0.3",
402
+ "etag": "~1.8.1",
403
+ "finalhandler": "1.3.1",
404
+ "fresh": "0.5.2",
405
+ "http-errors": "2.0.0",
406
+ "merge-descriptors": "1.0.3",
407
+ "methods": "~1.1.2",
408
+ "on-finished": "2.4.1",
409
+ "parseurl": "~1.3.3",
410
+ "path-to-regexp": "0.1.12",
411
+ "proxy-addr": "~2.0.7",
412
+ "qs": "6.13.0",
413
+ "range-parser": "~1.2.1",
414
+ "safe-buffer": "5.2.1",
415
+ "send": "0.19.0",
416
+ "serve-static": "1.16.2",
417
+ "setprototypeof": "1.2.0",
418
+ "statuses": "2.0.1",
419
+ "type-is": "~1.6.18",
420
+ "utils-merge": "1.0.1",
421
+ "vary": "~1.1.2"
422
+ },
423
+ "engines": {
424
+ "node": ">= 0.10.0"
425
+ },
426
+ "funding": {
427
+ "type": "opencollective",
428
+ "url": "https://opencollective.com/express"
429
+ }
430
+ },
431
+ "node_modules/fill-range": {
432
+ "version": "7.1.1",
433
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
434
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
435
+ "dev": true,
436
+ "license": "MIT",
437
+ "dependencies": {
438
+ "to-regex-range": "^5.0.1"
439
+ },
440
+ "engines": {
441
+ "node": ">=8"
442
+ }
443
+ },
444
+ "node_modules/finalhandler": {
445
+ "version": "1.3.1",
446
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
447
+ "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
448
+ "license": "MIT",
449
+ "dependencies": {
450
+ "debug": "2.6.9",
451
+ "encodeurl": "~2.0.0",
452
+ "escape-html": "~1.0.3",
453
+ "on-finished": "2.4.1",
454
+ "parseurl": "~1.3.3",
455
+ "statuses": "2.0.1",
456
+ "unpipe": "~1.0.0"
457
+ },
458
+ "engines": {
459
+ "node": ">= 0.8"
460
+ }
461
+ },
462
+ "node_modules/forwarded": {
463
+ "version": "0.2.0",
464
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
465
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
466
+ "license": "MIT",
467
+ "engines": {
468
+ "node": ">= 0.6"
469
+ }
470
+ },
471
+ "node_modules/fresh": {
472
+ "version": "0.5.2",
473
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
474
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
475
+ "license": "MIT",
476
+ "engines": {
477
+ "node": ">= 0.6"
478
+ }
479
+ },
480
+ "node_modules/fsevents": {
481
+ "version": "2.3.3",
482
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
483
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
484
+ "dev": true,
485
+ "hasInstallScript": true,
486
+ "license": "MIT",
487
+ "optional": true,
488
+ "os": [
489
+ "darwin"
490
+ ],
491
+ "engines": {
492
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
493
+ }
494
+ },
495
+ "node_modules/function-bind": {
496
+ "version": "1.1.2",
497
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
498
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
499
+ "license": "MIT",
500
+ "funding": {
501
+ "url": "https://github.com/sponsors/ljharb"
502
+ }
503
+ },
504
+ "node_modules/get-intrinsic": {
505
+ "version": "1.3.0",
506
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
507
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
508
+ "license": "MIT",
509
+ "dependencies": {
510
+ "call-bind-apply-helpers": "^1.0.2",
511
+ "es-define-property": "^1.0.1",
512
+ "es-errors": "^1.3.0",
513
+ "es-object-atoms": "^1.1.1",
514
+ "function-bind": "^1.1.2",
515
+ "get-proto": "^1.0.1",
516
+ "gopd": "^1.2.0",
517
+ "has-symbols": "^1.1.0",
518
+ "hasown": "^2.0.2",
519
+ "math-intrinsics": "^1.1.0"
520
+ },
521
+ "engines": {
522
+ "node": ">= 0.4"
523
+ },
524
+ "funding": {
525
+ "url": "https://github.com/sponsors/ljharb"
526
+ }
527
+ },
528
+ "node_modules/get-proto": {
529
+ "version": "1.0.1",
530
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
531
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
532
+ "license": "MIT",
533
+ "dependencies": {
534
+ "dunder-proto": "^1.0.1",
535
+ "es-object-atoms": "^1.0.0"
536
+ },
537
+ "engines": {
538
+ "node": ">= 0.4"
539
+ }
540
+ },
541
+ "node_modules/glob-parent": {
542
+ "version": "5.1.2",
543
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
544
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
545
+ "dev": true,
546
+ "license": "ISC",
547
+ "dependencies": {
548
+ "is-glob": "^4.0.1"
549
+ },
550
+ "engines": {
551
+ "node": ">= 6"
552
+ }
553
+ },
554
+ "node_modules/gopd": {
555
+ "version": "1.2.0",
556
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
557
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
558
+ "license": "MIT",
559
+ "engines": {
560
+ "node": ">= 0.4"
561
+ },
562
+ "funding": {
563
+ "url": "https://github.com/sponsors/ljharb"
564
+ }
565
+ },
566
+ "node_modules/has-flag": {
567
+ "version": "3.0.0",
568
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
569
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
570
+ "dev": true,
571
+ "license": "MIT",
572
+ "engines": {
573
+ "node": ">=4"
574
+ }
575
+ },
576
+ "node_modules/has-symbols": {
577
+ "version": "1.1.0",
578
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
579
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
580
+ "license": "MIT",
581
+ "engines": {
582
+ "node": ">= 0.4"
583
+ },
584
+ "funding": {
585
+ "url": "https://github.com/sponsors/ljharb"
586
+ }
587
+ },
588
+ "node_modules/hasown": {
589
+ "version": "2.0.2",
590
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
591
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
592
+ "license": "MIT",
593
+ "dependencies": {
594
+ "function-bind": "^1.1.2"
595
+ },
596
+ "engines": {
597
+ "node": ">= 0.4"
598
+ }
599
+ },
600
+ "node_modules/helmet": {
601
+ "version": "7.2.0",
602
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.2.0.tgz",
603
+ "integrity": "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw==",
604
+ "license": "MIT",
605
+ "engines": {
606
+ "node": ">=16.0.0"
607
+ }
608
+ },
609
+ "node_modules/http-errors": {
610
+ "version": "2.0.0",
611
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
612
+ "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
613
+ "license": "MIT",
614
+ "dependencies": {
615
+ "depd": "2.0.0",
616
+ "inherits": "2.0.4",
617
+ "setprototypeof": "1.2.0",
618
+ "statuses": "2.0.1",
619
+ "toidentifier": "1.0.1"
620
+ },
621
+ "engines": {
622
+ "node": ">= 0.8"
623
+ }
624
+ },
625
+ "node_modules/iconv-lite": {
626
+ "version": "0.4.24",
627
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
628
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
629
+ "license": "MIT",
630
+ "dependencies": {
631
+ "safer-buffer": ">= 2.1.2 < 3"
632
+ },
633
+ "engines": {
634
+ "node": ">=0.10.0"
635
+ }
636
+ },
637
+ "node_modules/ignore-by-default": {
638
+ "version": "1.0.1",
639
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
640
+ "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
641
+ "dev": true,
642
+ "license": "ISC"
643
+ },
644
+ "node_modules/inherits": {
645
+ "version": "2.0.4",
646
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
647
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
648
+ "license": "ISC"
649
+ },
650
+ "node_modules/ipaddr.js": {
651
+ "version": "1.9.1",
652
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
653
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
654
+ "license": "MIT",
655
+ "engines": {
656
+ "node": ">= 0.10"
657
+ }
658
+ },
659
+ "node_modules/is-binary-path": {
660
+ "version": "2.1.0",
661
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
662
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
663
+ "dev": true,
664
+ "license": "MIT",
665
+ "dependencies": {
666
+ "binary-extensions": "^2.0.0"
667
+ },
668
+ "engines": {
669
+ "node": ">=8"
670
+ }
671
+ },
672
+ "node_modules/is-extglob": {
673
+ "version": "2.1.1",
674
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
675
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
676
+ "dev": true,
677
+ "license": "MIT",
678
+ "engines": {
679
+ "node": ">=0.10.0"
680
+ }
681
+ },
682
+ "node_modules/is-glob": {
683
+ "version": "4.0.3",
684
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
685
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
686
+ "dev": true,
687
+ "license": "MIT",
688
+ "dependencies": {
689
+ "is-extglob": "^2.1.1"
690
+ },
691
+ "engines": {
692
+ "node": ">=0.10.0"
693
+ }
694
+ },
695
+ "node_modules/is-number": {
696
+ "version": "7.0.0",
697
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
698
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
699
+ "dev": true,
700
+ "license": "MIT",
701
+ "engines": {
702
+ "node": ">=0.12.0"
703
+ }
704
+ },
705
+ "node_modules/math-intrinsics": {
706
+ "version": "1.1.0",
707
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
708
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
709
+ "license": "MIT",
710
+ "engines": {
711
+ "node": ">= 0.4"
712
+ }
713
+ },
714
+ "node_modules/media-typer": {
715
+ "version": "0.3.0",
716
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
717
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
718
+ "license": "MIT",
719
+ "engines": {
720
+ "node": ">= 0.6"
721
+ }
722
+ },
723
+ "node_modules/merge-descriptors": {
724
+ "version": "1.0.3",
725
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
726
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
727
+ "license": "MIT",
728
+ "funding": {
729
+ "url": "https://github.com/sponsors/sindresorhus"
730
+ }
731
+ },
732
+ "node_modules/methods": {
733
+ "version": "1.1.2",
734
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
735
+ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
736
+ "license": "MIT",
737
+ "engines": {
738
+ "node": ">= 0.6"
739
+ }
740
+ },
741
+ "node_modules/mime": {
742
+ "version": "1.6.0",
743
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
744
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
745
+ "license": "MIT",
746
+ "bin": {
747
+ "mime": "cli.js"
748
+ },
749
+ "engines": {
750
+ "node": ">=4"
751
+ }
752
+ },
753
+ "node_modules/mime-db": {
754
+ "version": "1.54.0",
755
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
756
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
757
+ "license": "MIT",
758
+ "engines": {
759
+ "node": ">= 0.6"
760
+ }
761
+ },
762
+ "node_modules/mime-types": {
763
+ "version": "2.1.35",
764
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
765
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
766
+ "license": "MIT",
767
+ "dependencies": {
768
+ "mime-db": "1.52.0"
769
+ },
770
+ "engines": {
771
+ "node": ">= 0.6"
772
+ }
773
+ },
774
+ "node_modules/mime-types/node_modules/mime-db": {
775
+ "version": "1.52.0",
776
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
777
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
778
+ "license": "MIT",
779
+ "engines": {
780
+ "node": ">= 0.6"
781
+ }
782
+ },
783
+ "node_modules/minimatch": {
784
+ "version": "3.1.2",
785
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
786
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
787
+ "dev": true,
788
+ "license": "ISC",
789
+ "dependencies": {
790
+ "brace-expansion": "^1.1.7"
791
+ },
792
+ "engines": {
793
+ "node": "*"
794
+ }
795
+ },
796
+ "node_modules/ms": {
797
+ "version": "2.0.0",
798
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
799
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
800
+ "license": "MIT"
801
+ },
802
+ "node_modules/negotiator": {
803
+ "version": "0.6.4",
804
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
805
+ "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==",
806
+ "license": "MIT",
807
+ "engines": {
808
+ "node": ">= 0.6"
809
+ }
810
+ },
811
+ "node_modules/nodemon": {
812
+ "version": "3.1.10",
813
+ "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
814
+ "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==",
815
+ "dev": true,
816
+ "license": "MIT",
817
+ "dependencies": {
818
+ "chokidar": "^3.5.2",
819
+ "debug": "^4",
820
+ "ignore-by-default": "^1.0.1",
821
+ "minimatch": "^3.1.2",
822
+ "pstree.remy": "^1.1.8",
823
+ "semver": "^7.5.3",
824
+ "simple-update-notifier": "^2.0.0",
825
+ "supports-color": "^5.5.0",
826
+ "touch": "^3.1.0",
827
+ "undefsafe": "^2.0.5"
828
+ },
829
+ "bin": {
830
+ "nodemon": "bin/nodemon.js"
831
+ },
832
+ "engines": {
833
+ "node": ">=10"
834
+ },
835
+ "funding": {
836
+ "type": "opencollective",
837
+ "url": "https://opencollective.com/nodemon"
838
+ }
839
+ },
840
+ "node_modules/nodemon/node_modules/debug": {
841
+ "version": "4.4.1",
842
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
843
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
844
+ "dev": true,
845
+ "license": "MIT",
846
+ "dependencies": {
847
+ "ms": "^2.1.3"
848
+ },
849
+ "engines": {
850
+ "node": ">=6.0"
851
+ },
852
+ "peerDependenciesMeta": {
853
+ "supports-color": {
854
+ "optional": true
855
+ }
856
+ }
857
+ },
858
+ "node_modules/nodemon/node_modules/ms": {
859
+ "version": "2.1.3",
860
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
861
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
862
+ "dev": true,
863
+ "license": "MIT"
864
+ },
865
+ "node_modules/normalize-path": {
866
+ "version": "3.0.0",
867
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
868
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
869
+ "dev": true,
870
+ "license": "MIT",
871
+ "engines": {
872
+ "node": ">=0.10.0"
873
+ }
874
+ },
875
+ "node_modules/object-assign": {
876
+ "version": "4.1.1",
877
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
878
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
879
+ "license": "MIT",
880
+ "engines": {
881
+ "node": ">=0.10.0"
882
+ }
883
+ },
884
+ "node_modules/object-inspect": {
885
+ "version": "1.13.4",
886
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
887
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
888
+ "license": "MIT",
889
+ "engines": {
890
+ "node": ">= 0.4"
891
+ },
892
+ "funding": {
893
+ "url": "https://github.com/sponsors/ljharb"
894
+ }
895
+ },
896
+ "node_modules/on-finished": {
897
+ "version": "2.4.1",
898
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
899
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
900
+ "license": "MIT",
901
+ "dependencies": {
902
+ "ee-first": "1.1.1"
903
+ },
904
+ "engines": {
905
+ "node": ">= 0.8"
906
+ }
907
+ },
908
+ "node_modules/on-headers": {
909
+ "version": "1.1.0",
910
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
911
+ "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
912
+ "license": "MIT",
913
+ "engines": {
914
+ "node": ">= 0.8"
915
+ }
916
+ },
917
+ "node_modules/parseurl": {
918
+ "version": "1.3.3",
919
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
920
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
921
+ "license": "MIT",
922
+ "engines": {
923
+ "node": ">= 0.8"
924
+ }
925
+ },
926
+ "node_modules/path-to-regexp": {
927
+ "version": "0.1.12",
928
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
929
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
930
+ "license": "MIT"
931
+ },
932
+ "node_modules/picomatch": {
933
+ "version": "2.3.1",
934
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
935
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
936
+ "dev": true,
937
+ "license": "MIT",
938
+ "engines": {
939
+ "node": ">=8.6"
940
+ },
941
+ "funding": {
942
+ "url": "https://github.com/sponsors/jonschlinkert"
943
+ }
944
+ },
945
+ "node_modules/proxy-addr": {
946
+ "version": "2.0.7",
947
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
948
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
949
+ "license": "MIT",
950
+ "dependencies": {
951
+ "forwarded": "0.2.0",
952
+ "ipaddr.js": "1.9.1"
953
+ },
954
+ "engines": {
955
+ "node": ">= 0.10"
956
+ }
957
+ },
958
+ "node_modules/pstree.remy": {
959
+ "version": "1.1.8",
960
+ "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
961
+ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
962
+ "dev": true,
963
+ "license": "MIT"
964
+ },
965
+ "node_modules/qs": {
966
+ "version": "6.13.0",
967
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
968
+ "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
969
+ "license": "BSD-3-Clause",
970
+ "dependencies": {
971
+ "side-channel": "^1.0.6"
972
+ },
973
+ "engines": {
974
+ "node": ">=0.6"
975
+ },
976
+ "funding": {
977
+ "url": "https://github.com/sponsors/ljharb"
978
+ }
979
+ },
980
+ "node_modules/range-parser": {
981
+ "version": "1.2.1",
982
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
983
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
984
+ "license": "MIT",
985
+ "engines": {
986
+ "node": ">= 0.6"
987
+ }
988
+ },
989
+ "node_modules/raw-body": {
990
+ "version": "2.5.2",
991
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
992
+ "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
993
+ "license": "MIT",
994
+ "dependencies": {
995
+ "bytes": "3.1.2",
996
+ "http-errors": "2.0.0",
997
+ "iconv-lite": "0.4.24",
998
+ "unpipe": "1.0.0"
999
+ },
1000
+ "engines": {
1001
+ "node": ">= 0.8"
1002
+ }
1003
+ },
1004
+ "node_modules/readdirp": {
1005
+ "version": "3.6.0",
1006
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
1007
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
1008
+ "dev": true,
1009
+ "license": "MIT",
1010
+ "dependencies": {
1011
+ "picomatch": "^2.2.1"
1012
+ },
1013
+ "engines": {
1014
+ "node": ">=8.10.0"
1015
+ }
1016
+ },
1017
+ "node_modules/safe-buffer": {
1018
+ "version": "5.2.1",
1019
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1020
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1021
+ "funding": [
1022
+ {
1023
+ "type": "github",
1024
+ "url": "https://github.com/sponsors/feross"
1025
+ },
1026
+ {
1027
+ "type": "patreon",
1028
+ "url": "https://www.patreon.com/feross"
1029
+ },
1030
+ {
1031
+ "type": "consulting",
1032
+ "url": "https://feross.org/support"
1033
+ }
1034
+ ],
1035
+ "license": "MIT"
1036
+ },
1037
+ "node_modules/safer-buffer": {
1038
+ "version": "2.1.2",
1039
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1040
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1041
+ "license": "MIT"
1042
+ },
1043
+ "node_modules/semver": {
1044
+ "version": "7.7.2",
1045
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
1046
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
1047
+ "dev": true,
1048
+ "license": "ISC",
1049
+ "bin": {
1050
+ "semver": "bin/semver.js"
1051
+ },
1052
+ "engines": {
1053
+ "node": ">=10"
1054
+ }
1055
+ },
1056
+ "node_modules/send": {
1057
+ "version": "0.19.0",
1058
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1059
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1060
+ "license": "MIT",
1061
+ "dependencies": {
1062
+ "debug": "2.6.9",
1063
+ "depd": "2.0.0",
1064
+ "destroy": "1.2.0",
1065
+ "encodeurl": "~1.0.2",
1066
+ "escape-html": "~1.0.3",
1067
+ "etag": "~1.8.1",
1068
+ "fresh": "0.5.2",
1069
+ "http-errors": "2.0.0",
1070
+ "mime": "1.6.0",
1071
+ "ms": "2.1.3",
1072
+ "on-finished": "2.4.1",
1073
+ "range-parser": "~1.2.1",
1074
+ "statuses": "2.0.1"
1075
+ },
1076
+ "engines": {
1077
+ "node": ">= 0.8.0"
1078
+ }
1079
+ },
1080
+ "node_modules/send/node_modules/encodeurl": {
1081
+ "version": "1.0.2",
1082
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1083
+ "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
1084
+ "license": "MIT",
1085
+ "engines": {
1086
+ "node": ">= 0.8"
1087
+ }
1088
+ },
1089
+ "node_modules/send/node_modules/ms": {
1090
+ "version": "2.1.3",
1091
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1092
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1093
+ "license": "MIT"
1094
+ },
1095
+ "node_modules/serve-static": {
1096
+ "version": "1.16.2",
1097
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1098
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1099
+ "license": "MIT",
1100
+ "dependencies": {
1101
+ "encodeurl": "~2.0.0",
1102
+ "escape-html": "~1.0.3",
1103
+ "parseurl": "~1.3.3",
1104
+ "send": "0.19.0"
1105
+ },
1106
+ "engines": {
1107
+ "node": ">= 0.8.0"
1108
+ }
1109
+ },
1110
+ "node_modules/setprototypeof": {
1111
+ "version": "1.2.0",
1112
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1113
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1114
+ "license": "ISC"
1115
+ },
1116
+ "node_modules/side-channel": {
1117
+ "version": "1.1.0",
1118
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1119
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1120
+ "license": "MIT",
1121
+ "dependencies": {
1122
+ "es-errors": "^1.3.0",
1123
+ "object-inspect": "^1.13.3",
1124
+ "side-channel-list": "^1.0.0",
1125
+ "side-channel-map": "^1.0.1",
1126
+ "side-channel-weakmap": "^1.0.2"
1127
+ },
1128
+ "engines": {
1129
+ "node": ">= 0.4"
1130
+ },
1131
+ "funding": {
1132
+ "url": "https://github.com/sponsors/ljharb"
1133
+ }
1134
+ },
1135
+ "node_modules/side-channel-list": {
1136
+ "version": "1.0.0",
1137
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1138
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1139
+ "license": "MIT",
1140
+ "dependencies": {
1141
+ "es-errors": "^1.3.0",
1142
+ "object-inspect": "^1.13.3"
1143
+ },
1144
+ "engines": {
1145
+ "node": ">= 0.4"
1146
+ },
1147
+ "funding": {
1148
+ "url": "https://github.com/sponsors/ljharb"
1149
+ }
1150
+ },
1151
+ "node_modules/side-channel-map": {
1152
+ "version": "1.0.1",
1153
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1154
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1155
+ "license": "MIT",
1156
+ "dependencies": {
1157
+ "call-bound": "^1.0.2",
1158
+ "es-errors": "^1.3.0",
1159
+ "get-intrinsic": "^1.2.5",
1160
+ "object-inspect": "^1.13.3"
1161
+ },
1162
+ "engines": {
1163
+ "node": ">= 0.4"
1164
+ },
1165
+ "funding": {
1166
+ "url": "https://github.com/sponsors/ljharb"
1167
+ }
1168
+ },
1169
+ "node_modules/side-channel-weakmap": {
1170
+ "version": "1.0.2",
1171
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1172
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1173
+ "license": "MIT",
1174
+ "dependencies": {
1175
+ "call-bound": "^1.0.2",
1176
+ "es-errors": "^1.3.0",
1177
+ "get-intrinsic": "^1.2.5",
1178
+ "object-inspect": "^1.13.3",
1179
+ "side-channel-map": "^1.0.1"
1180
+ },
1181
+ "engines": {
1182
+ "node": ">= 0.4"
1183
+ },
1184
+ "funding": {
1185
+ "url": "https://github.com/sponsors/ljharb"
1186
+ }
1187
+ },
1188
+ "node_modules/simple-update-notifier": {
1189
+ "version": "2.0.0",
1190
+ "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
1191
+ "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
1192
+ "dev": true,
1193
+ "license": "MIT",
1194
+ "dependencies": {
1195
+ "semver": "^7.5.3"
1196
+ },
1197
+ "engines": {
1198
+ "node": ">=10"
1199
+ }
1200
+ },
1201
+ "node_modules/statuses": {
1202
+ "version": "2.0.1",
1203
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1204
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1205
+ "license": "MIT",
1206
+ "engines": {
1207
+ "node": ">= 0.8"
1208
+ }
1209
+ },
1210
+ "node_modules/supports-color": {
1211
+ "version": "5.5.0",
1212
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
1213
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
1214
+ "dev": true,
1215
+ "license": "MIT",
1216
+ "dependencies": {
1217
+ "has-flag": "^3.0.0"
1218
+ },
1219
+ "engines": {
1220
+ "node": ">=4"
1221
+ }
1222
+ },
1223
+ "node_modules/to-regex-range": {
1224
+ "version": "5.0.1",
1225
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
1226
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
1227
+ "dev": true,
1228
+ "license": "MIT",
1229
+ "dependencies": {
1230
+ "is-number": "^7.0.0"
1231
+ },
1232
+ "engines": {
1233
+ "node": ">=8.0"
1234
+ }
1235
+ },
1236
+ "node_modules/toidentifier": {
1237
+ "version": "1.0.1",
1238
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1239
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1240
+ "license": "MIT",
1241
+ "engines": {
1242
+ "node": ">=0.6"
1243
+ }
1244
+ },
1245
+ "node_modules/touch": {
1246
+ "version": "3.1.1",
1247
+ "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
1248
+ "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
1249
+ "dev": true,
1250
+ "license": "ISC",
1251
+ "bin": {
1252
+ "nodetouch": "bin/nodetouch.js"
1253
+ }
1254
+ },
1255
+ "node_modules/type-is": {
1256
+ "version": "1.6.18",
1257
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1258
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1259
+ "license": "MIT",
1260
+ "dependencies": {
1261
+ "media-typer": "0.3.0",
1262
+ "mime-types": "~2.1.24"
1263
+ },
1264
+ "engines": {
1265
+ "node": ">= 0.6"
1266
+ }
1267
+ },
1268
+ "node_modules/undefsafe": {
1269
+ "version": "2.0.5",
1270
+ "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
1271
+ "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
1272
+ "dev": true,
1273
+ "license": "MIT"
1274
+ },
1275
+ "node_modules/unpipe": {
1276
+ "version": "1.0.0",
1277
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
1278
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
1279
+ "license": "MIT",
1280
+ "engines": {
1281
+ "node": ">= 0.8"
1282
+ }
1283
+ },
1284
+ "node_modules/utils-merge": {
1285
+ "version": "1.0.1",
1286
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
1287
+ "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
1288
+ "license": "MIT",
1289
+ "engines": {
1290
+ "node": ">= 0.4.0"
1291
+ }
1292
+ },
1293
+ "node_modules/vary": {
1294
+ "version": "1.1.2",
1295
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
1296
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
1297
+ "license": "MIT",
1298
+ "engines": {
1299
+ "node": ">= 0.8"
1300
+ }
1301
+ }
1302
+ }
1303
+ }
package.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "30-day-keto-planner",
3
+ "version": "1.0.0",
4
+ "description": "30-Day Ketogenic Weight Loss Planner - Complete daily tracker with motivation and guidance",
5
+ "main": "server.js",
6
+ "scripts": {
7
+ "start": "node server.js",
8
+ "dev": "nodemon server.js",
9
+ "build": "node scripts/build-production.js",
10
+ "build:analyze": "npm run build && npm run analyze",
11
+ "minify": "node scripts/minify.js",
12
+ "analyze": "node scripts/analyze-bundle.js",
13
+ "optimize": "node scripts/build-production.js && node scripts/optimize-images.js",
14
+ "test": "echo 'No tests specified'",
15
+ "lighthouse": "lighthouse http://localhost:7860 --view --output html --output-path ./lighthouse-report.html"
16
+ },
17
+ "keywords": [
18
+ "ketogenic",
19
+ "weight-loss",
20
+ "planner",
21
+ "health",
22
+ "fitness"
23
+ ],
24
+ "author": "30-Day Keto Planner",
25
+ "license": "MIT",
26
+ "dependencies": {
27
+ "express": "^4.18.2",
28
+ "cors": "^2.8.5",
29
+ "helmet": "^7.0.0",
30
+ "compression": "^1.7.4"
31
+ },
32
+ "devDependencies": {
33
+ "nodemon": "^3.0.1"
34
+ },
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ }
38
+ }
public/app.js ADDED
The diff for this file is too large to render. See raw diff
 
public/icons/icon-128x128.svg ADDED
public/icons/icon-144x144.svg ADDED
public/icons/icon-152x152.svg ADDED
public/icons/icon-192x192.svg ADDED
public/icons/icon-384x384.svg ADDED
public/icons/icon-512x512.svg ADDED
public/icons/icon-72x72.png ADDED
public/icons/icon-72x72.svg ADDED
public/icons/icon-96x96.svg ADDED
public/icons/icon.svg ADDED
public/index.html ADDED
@@ -0,0 +1,746 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover">
6
+ <meta name="mobile-web-app-capable" content="yes">
7
+ <meta name="apple-mobile-web-app-capable" content="yes">
8
+ <meta name="apple-mobile-web-app-title" content="Fitness App">
9
+ <title>✨ Sua Jornada de Transformação</title>
10
+
11
+ <!-- Performance: DNS Prefetch for external resources -->
12
+ <link rel="dns-prefetch" href="https://fonts.googleapis.com">
13
+ <link rel="dns-prefetch" href="https://fonts.gstatic.com">
14
+ <link rel="dns-prefetch" href="https://huggingface.co">
15
+
16
+ <!-- Performance: Preconnect for fonts (saves ~300ms) -->
17
+ <link rel="preconnect" href="https://fonts.googleapis.com">
18
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19
+
20
+ <!-- Performance: Preconnect for Hugging Face videos (critical for workout videos) -->
21
+ <link rel="preconnect" href="https://huggingface.co">
22
+ <link rel="preconnect" href="https://cdn-lfs.huggingface.co" crossorigin>
23
+
24
+ <!-- Performance: Optimized font loading with font-display -->
25
+ <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&family=Playfair+Display:wght@400;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'">
26
+ <noscript><link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&family=Playfair+Display:wght@400;600;700&display=swap" rel="stylesheet"></noscript>
27
+
28
+ <!-- Performance: Preload critical CSS -->
29
+ <link rel="preload" href="styles.css" as="style">
30
+ <link rel="stylesheet" href="styles.css">
31
+
32
+ <!-- Performance: Preload critical JavaScript -->
33
+ <link rel="modulepreload" href="app.js">
34
+ <link rel="modulepreload" href="lazy-video.js">
35
+
36
+ <!-- PWA Manifest -->
37
+ <link rel="manifest" href="manifest.json">
38
+
39
+ <meta name="description" content="Seu aplicativo de transformação pessoal completo - exercícios, nutrição e bem-estar">
40
+ <meta name="keywords" content="fitness, treino, nutrição, bem-estar, yoga, meditação, saúde">
41
+ <meta name="theme-color" content="#FF6B9D" media="(prefers-color-scheme: light)">
42
+ <meta name="theme-color" content="#1a1a1a" media="(prefers-color-scheme: dark)">
43
+ <meta name="apple-mobile-web-app-capable" content="yes">
44
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
45
+
46
+ <!-- PWA: Apple Touch Icons -->
47
+ <link rel="apple-touch-icon" sizes="192x192" href="icons/icon-192x192.svg">
48
+ <link rel="apple-touch-icon" sizes="512x512" href="icons/icon-512x512.svg">
49
+
50
+ <!-- PWA: Microsoft Tiles -->
51
+ <meta name="msapplication-TileColor" content="#FF6B9D">
52
+ <meta name="msapplication-config" content="none">
53
+
54
+ <!-- Performance: Preload favicon -->
55
+ <link rel="icon" type="image/svg+xml" href="icons/icon.svg">
56
+
57
+ <!-- Performance: Resource hints -->
58
+ <link rel="preload" href="app.js" as="script">
59
+ <link rel="modulepreload" href="app.js">
60
+ </head>
61
+ <body>
62
+ <!-- Welcome Screen -->
63
+ <div class="welcome-screen" id="welcomeScreen">
64
+ <div class="welcome-content">
65
+ <div class="welcome-logo">
66
+ <div class="logo-heart">💖</div>
67
+ <h1>Bem-vinda!</h1>
68
+ <p>Sua jornada de transformação começa aqui</p>
69
+ </div>
70
+ <button class="btn-start-journey" id="startJourney">Começar Agora ✨</button>
71
+ </div>
72
+ </div>
73
+
74
+ <div class="app-container" id="appContainer" style="display: none;">
75
+ <!-- Top Bar -->
76
+ <header class="top-bar">
77
+ <div class="user-info" style="cursor: pointer;" id="userProfileClick">
78
+ <div class="avatar" id="userAvatar">💝</div>
79
+ <div class="user-text">
80
+ <span class="greeting" id="greeting">Olá, Guerreira!</span>
81
+ <span class="streak">🔥 <span id="streakDays">0</span> dias seguidos</span>
82
+ </div>
83
+ </div>
84
+ <div class="top-actions">
85
+ <button class="icon-btn" id="notifBtn" title="Notificações">
86
+ <span>🔔</span>
87
+ <span class="notification-badge" id="notificationBadge" style="display: none;">0</span>
88
+ </button>
89
+ </div>
90
+ </header>
91
+
92
+ <!-- Main Content -->
93
+ <main class="main-view">
94
+ <!-- Home View -->
95
+ <section class="view active" id="homeView">
96
+ <div class="hero-section">
97
+ <h2 class="page-title">Sua Transformação</h2>
98
+ <div class="daily-progress">
99
+ <div class="progress-circle" id="progressCircle">
100
+ <svg viewBox="0 0 120 120">
101
+ <circle cx="60" cy="60" r="54" class="progress-bg"></circle>
102
+ <circle cx="60" cy="60" r="54" class="progress-fill" id="progressFill" style="--progress: 0"></circle>
103
+ </svg>
104
+ <div class="progress-text">
105
+ <span class="progress-value" id="progressValue">0</span>%
106
+ <span class="progress-label">Hoje</span>
107
+ </div>
108
+ </div>
109
+ <div class="today-stats">
110
+ <div class="stat">
111
+ <span class="stat-icon">🔥</span>
112
+ <div>
113
+ <span class="stat-value" id="caloriesBurned">0</span>
114
+ <span class="stat-label">kcal</span>
115
+ </div>
116
+ </div>
117
+ <div class="stat">
118
+ <span class="stat-icon">⏱️</span>
119
+ <div>
120
+ <span class="stat-value" id="minutesActive">0</span>
121
+ <span class="stat-label">min</span>
122
+ </div>
123
+ </div>
124
+ <div class="stat">
125
+ <span class="stat-icon">💪</span>
126
+ <div>
127
+ <span class="stat-value" id="workoutsCompleted">0</span>
128
+ <span class="stat-label">treinos</span>
129
+ </div>
130
+ </div>
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <!-- Personal Plan Summary -->
136
+ <div class="plan-summary" id="planSummary" style="display: none;">
137
+ <div class="plan-card">
138
+ <div class="plan-header">
139
+ <h3>🎯 Seu Plano Personalizado</h3>
140
+ <button class="btn-view-plan" id="viewFullPlan">Ver Detalhes</button>
141
+ </div>
142
+ <div class="plan-quick-stats">
143
+ <div class="plan-stat">
144
+ <span class="plan-label">Meta</span>
145
+ <span class="plan-value" id="planGoal">-</span>
146
+ </div>
147
+ <div class="plan-stat">
148
+ <span class="plan-label">Calorias/dia</span>
149
+ <span class="plan-value" id="planCalories">-</span>
150
+ </div>
151
+ <div class="plan-stat">
152
+ <span class="plan-label">Treino</span>
153
+ <span class="plan-value" id="planWorkout">-</span>
154
+ </div>
155
+ </div>
156
+ <div class="coach-message" id="coachMessage">
157
+ 💪 Continue assim! Você está no caminho certo!
158
+ </div>
159
+ </div>
160
+ </div>
161
+
162
+ <div class="quick-actions">
163
+ <h3 class="section-title">Comece Agora</h3>
164
+ <div class="action-cards">
165
+ <div class="action-card" data-navigate="workouts">
166
+ <div class="action-icon">💪</div>
167
+ <h4>Treinar</h4>
168
+ <p>Escolha sua área</p>
169
+ </div>
170
+ <div class="action-card" data-navigate="nutrition">
171
+ <div class="action-icon">🥗</div>
172
+ <h4>Nutrição</h4>
173
+ <p>Acompanhe sua dieta</p>
174
+ </div>
175
+ <div class="action-card" data-navigate="wellness">
176
+ <div class="action-icon">🧘‍♀️</div>
177
+ <h4>Bem-Estar</h4>
178
+ <p>Massagens & Postura</p>
179
+ </div>
180
+ <div class="action-card" data-navigate="progress">
181
+ <div class="action-icon">📈</div>
182
+ <h4>Progresso</h4>
183
+ <p>Veja sua evolução</p>
184
+ </div>
185
+ </div>
186
+ </div>
187
+
188
+ <div class="motivation-card">
189
+ <div class="motivation-icon">✨</div>
190
+ <p class="motivation-text" id="dailyMotivation">Você é capaz de coisas incríveis! Continue brilhando! 💫</p>
191
+ </div>
192
+ </section>
193
+
194
+ <!-- Workouts View -->
195
+ <section class="view" id="workoutsView">
196
+ <div class="view-header">
197
+ <button class="btn-back" data-back="home">← Voltar</button>
198
+ <h2 class="view-title">Escolha a Área</h2>
199
+ </div>
200
+
201
+ <div class="category-grid">
202
+ <div class="category-card" data-category="personalized" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
203
+ <div class="category-image">🎯</div>
204
+ <h3>Personalizado</h3>
205
+ <p>Seu treino único</p>
206
+ <span class="category-badge" style="background: rgba(255,255,255,0.3);">14 exercícios</span>
207
+ </div>
208
+
209
+ <div class="category-card" data-category="abs">
210
+ <div class="category-image">🔥</div>
211
+ <h3>Abdômen</h3>
212
+ <p>Barriga definida</p>
213
+ <span class="category-badge">5 exercícios</span>
214
+ </div>
215
+
216
+ <div class="category-card" data-category="legs">
217
+ <div class="category-image">🦵</div>
218
+ <h3>Pernas</h3>
219
+ <p>Pernas torneadas</p>
220
+ <span class="category-badge">15 exercícios</span>
221
+ </div>
222
+
223
+ <div class="category-card" data-category="glutes">
224
+ <div class="category-image">🍑</div>
225
+ <h3>Glúteos</h3>
226
+ <p>Bumbum empinado</p>
227
+ <span class="category-badge">14 exercícios</span>
228
+ </div>
229
+
230
+ <div class="category-card" data-category="arms">
231
+ <div class="category-image">💪</div>
232
+ <h3>Braços</h3>
233
+ <p>Braços definidos</p>
234
+ <span class="category-badge">10 exercícios</span>
235
+ </div>
236
+
237
+ <div class="category-card" data-category="waist">
238
+ <div class="category-image">⏳</div>
239
+ <h3>Cintura</h3>
240
+ <p>Cintura fina</p>
241
+ <span class="category-badge">8 exercícios</span>
242
+ </div>
243
+
244
+ <div class="category-card" data-category="back">
245
+ <div class="category-image">🏋️‍♀️</div>
246
+ <h3>Costas</h3>
247
+ <p>Postura correta</p>
248
+ <span class="category-badge">9 exercícios</span>
249
+ </div>
250
+
251
+ <div class="category-card" data-category="cardio">
252
+ <div class="category-image">❤️</div>
253
+ <h3>Cardio</h3>
254
+ <p>Queime calorias</p>
255
+ <span class="category-badge">11 exercícios</span>
256
+ </div>
257
+
258
+ <div class="category-card" data-category="fullbody">
259
+ <div class="category-image">✨</div>
260
+ <h3>Corpo Todo</h3>
261
+ <p>Treino completo</p>
262
+ <span class="category-badge">20 exercícios</span>
263
+ </div>
264
+
265
+ <div class="category-card" data-category="yoga">
266
+ <div class="category-image">🧘‍♀️</div>
267
+ <h3>Yoga</h3>
268
+ <p>Flexibilidade e paz</p>
269
+ <span class="category-badge">12 posições</span>
270
+ </div>
271
+
272
+ <div class="category-card" data-category="massage">
273
+ <div class="category-image">💆‍♀️</div>
274
+ <h3>Massagem</h3>
275
+ <p>Relaxamento total</p>
276
+ <span class="category-badge">8 técnicas</span>
277
+ </div>
278
+
279
+ <div class="category-card" data-category="face">
280
+ <div class="category-image">😊</div>
281
+ <h3>Rosto</h3>
282
+ <p>Rejuvenescimento facial</p>
283
+ <span class="category-badge">4 exercícios</span>
284
+ </div>
285
+ </div>
286
+ </section>
287
+
288
+ <!-- Exercises List View -->
289
+ <section class="view" id="exercisesListView">
290
+ <div class="view-header">
291
+ <button class="btn-back" data-back="workouts">← Voltar</button>
292
+ <h2 class="view-title" id="categoryTitle">Exercícios</h2>
293
+ </div>
294
+
295
+ <div class="exercises-container" id="exercisesList">
296
+ <!-- Exercises will be populated here -->
297
+ </div>
298
+ </section>
299
+
300
+ <!-- Workout Session View -->
301
+ <section class="view" id="workoutSessionView">
302
+ <div class="workout-header">
303
+ <button class="btn-close-workout" id="closeWorkout">✕</button>
304
+ <div class="workout-timer" id="workoutTimer">00:00</div>
305
+ </div>
306
+
307
+ <div class="workout-content">
308
+ <div class="exercise-display">
309
+ <div class="exercise-name" id="currentExerciseName">Preparando...</div>
310
+ <div class="exercise-count" id="exerciseCount">Exercício 1 de 5</div>
311
+
312
+ <div class="exercise-demo" id="exerciseDemo">
313
+ <div class="demo-placeholder">
314
+ <video class="demo-video" id="demoVideo" loop muted playsinline style="display: none;">
315
+ <source src="" type="video/mp4">
316
+ </video>
317
+ <span class="demo-icon" id="demoIcon">💪</span>
318
+ </div>
319
+ </div>
320
+
321
+ <div class="exercise-instructions">
322
+ <div class="reps-info" id="repsInfo">3 séries × 15 repetições</div>
323
+ <div class="rest-info" id="restInfo">Descanso: 30s entre séries</div>
324
+ </div>
325
+
326
+ <div class="series-tracker" id="seriesTracker">
327
+ <div class="series-dot completed"></div>
328
+ <div class="series-dot"></div>
329
+ <div class="series-dot"></div>
330
+ </div>
331
+ </div>
332
+
333
+ <div class="workout-controls">
334
+ <button class="btn-workout-action secondary" id="skipExercise">Pular</button>
335
+ <button class="btn-workout-action primary" id="completeExercise">Concluir Série</button>
336
+ </div>
337
+ </div>
338
+
339
+ <div class="workout-progress-bar">
340
+ <div class="progress-bar-fill" id="workoutProgressBar" style="width: 0%"></div>
341
+ </div>
342
+ </section>
343
+
344
+ <!-- Wellness View -->
345
+ <section class="view" id="wellnessView">
346
+ <div class="view-header">
347
+ <button class="btn-back" data-back="home">← Voltar</button>
348
+ <h2 class="view-title">Bem-Estar</h2>
349
+ </div>
350
+
351
+ <div class="wellness-grid">
352
+ <div class="wellness-card" data-wellness="face-massage">
353
+ <div class="wellness-icon">💆‍♀️</div>
354
+ <h3>Massagem Facial</h3>
355
+ <p>Rejuvenesça e relaxe</p>
356
+ <span class="duration">10 min</span>
357
+ </div>
358
+
359
+ <div class="wellness-card" data-wellness="body-massage">
360
+ <div class="wellness-icon">💫</div>
361
+ <h3>Massagem Corporal</h3>
362
+ <p>Alivie tensões</p>
363
+ <span class="duration">15 min</span>
364
+ </div>
365
+
366
+ <div class="wellness-card" data-wellness="posture">
367
+ <div class="wellness-icon">🧍‍♀️</div>
368
+ <h3>Correção Postural</h3>
369
+ <p>Melhore sua postura</p>
370
+ <span class="duration">12 min</span>
371
+ </div>
372
+
373
+ <div class="wellness-card" data-wellness="stretching">
374
+ <div class="wellness-icon">🤸‍♀️</div>
375
+ <h3>Alongamento</h3>
376
+ <p>Flexibilidade total</p>
377
+ <span class="duration">8 min</span>
378
+ </div>
379
+
380
+ <div class="wellness-card" data-wellness="breathing">
381
+ <div class="wellness-icon">🌬️</div>
382
+ <h3>Respiração</h3>
383
+ <p>Acalme sua mente</p>
384
+ <span class="duration">5 min</span>
385
+ </div>
386
+
387
+ <div class="wellness-card" data-wellness="meditation">
388
+ <div class="wellness-icon">🧘‍♀️</div>
389
+ <h3>Meditação</h3>
390
+ <p>Paz interior</p>
391
+ <span class="duration">10 min</span>
392
+ </div>
393
+ </div>
394
+ </section>
395
+
396
+ <!-- Nutrition View -->
397
+ <section class="view" id="nutritionView">
398
+ <div class="view-header">
399
+ <button class="btn-back" data-back="home">← Voltar</button>
400
+ <h2 class="view-title">Nutrição</h2>
401
+ </div>
402
+
403
+ <div class="nutrition-summary">
404
+ <h3>Resumo de Hoje</h3>
405
+ <div class="macros-display">
406
+ <div class="macro-item">
407
+ <div class="macro-circle carbs">
408
+ <span id="carbsValue">0g</span>
409
+ </div>
410
+ <span class="macro-label">Carboidratos</span>
411
+ </div>
412
+ <div class="macro-item">
413
+ <div class="macro-circle protein">
414
+ <span id="proteinValue">0g</span>
415
+ </div>
416
+ <span class="macro-label">Proteínas</span>
417
+ </div>
418
+ <div class="macro-item">
419
+ <div class="macro-circle fat">
420
+ <span id="fatValue">0g</span>
421
+ </div>
422
+ <span class="macro-label">Gorduras</span>
423
+ </div>
424
+ </div>
425
+ <div class="calories-total">
426
+ <span class="calories-value" id="totalCalories">0</span>
427
+ <span class="calories-label">kcal hoje</span>
428
+ </div>
429
+ </div>
430
+
431
+ <div class="water-tracker">
432
+ <h3>Hidratação 💧</h3>
433
+ <div class="water-glasses">
434
+ <div class="glass" data-glass="1">💧</div>
435
+ <div class="glass" data-glass="2">💧</div>
436
+ <div class="glass" data-glass="3">💧</div>
437
+ <div class="glass" data-glass="4">💧</div>
438
+ <div class="glass" data-glass="5">💧</div>
439
+ <div class="glass" data-glass="6">💧</div>
440
+ <div class="glass" data-glass="7">💧</div>
441
+ <div class="glass" data-glass="8">💧</div>
442
+ </div>
443
+ <p class="water-goal"><span id="waterCount">0</span>/8 copos (2L)</p>
444
+ </div>
445
+ </section>
446
+
447
+ <!-- Progress View -->
448
+ <section class="view" id="progressView">
449
+ <div class="view-header">
450
+ <button class="btn-back" data-back="home">← Voltar</button>
451
+ <h2 class="view-title">Seu Progresso</h2>
452
+ </div>
453
+
454
+ <!-- Weight Tracking -->
455
+ <div class="weight-tracking-section">
456
+ <h3>Controle de Peso ⚖️</h3>
457
+ <div class="weight-card">
458
+ <div class="weight-current">
459
+ <div class="weight-label">Peso Atual</div>
460
+ <div class="weight-value" id="currentWeight">--</div>
461
+ <button class="btn-update-weight" id="updateWeightBtn">Atualizar Peso</button>
462
+ </div>
463
+ <div class="weight-stats">
464
+ <div class="weight-stat">
465
+ <div class="weight-stat-label">Peso Inicial</div>
466
+ <div class="weight-stat-value" id="initialWeight">--</div>
467
+ </div>
468
+ <div class="weight-stat success">
469
+ <div class="weight-stat-label">Perdeu</div>
470
+ <div class="weight-stat-value" id="weightLost">0 kg</div>
471
+ </div>
472
+ <div class="weight-stat">
473
+ <div class="weight-stat-label">Meta</div>
474
+ <div class="weight-stat-value" id="goalWeight">--</div>
475
+ </div>
476
+ </div>
477
+ <div class="weight-progress-bar">
478
+ <div class="weight-progress-fill" id="weightProgressFill" style="width: 0%"></div>
479
+ </div>
480
+ <div class="weight-chart-mini" id="weightChartMini">
481
+ <!-- Mini chart will be rendered here -->
482
+ </div>
483
+ </div>
484
+ </div>
485
+
486
+ <!-- Weekly Activity -->
487
+ <div class="weekly-activity-section">
488
+ <h3>Atividade Semanal 📅</h3>
489
+ <div class="weekly-activity-grid" id="weeklyActivityGrid">
490
+ <!-- Será preenchido dinamicamente -->
491
+ </div>
492
+ </div>
493
+
494
+ <!-- Detailed Statistics -->
495
+ <div class="detailed-stats-section">
496
+ <h3>Estatísticas Detalhadas 📊</h3>
497
+ <div class="stats-grid">
498
+ <div class="stat-detail-card">
499
+ <div class="stat-detail-icon">🔥</div>
500
+ <div class="stat-detail-content">
501
+ <div class="stat-detail-number" id="totalWorkouts">0</div>
502
+ <div class="stat-detail-label">Treinos Completos</div>
503
+ <div class="stat-detail-sublabel">
504
+ <span id="thisWeekWorkouts">0</span> esta semana
505
+ </div>
506
+ </div>
507
+ </div>
508
+
509
+ <div class="stat-detail-card">
510
+ <div class="stat-detail-icon">⏱️</div>
511
+ <div class="stat-detail-content">
512
+ <div class="stat-detail-number" id="totalMinutes">0</div>
513
+ <div class="stat-detail-label">Minutos Ativos</div>
514
+ <div class="stat-detail-sublabel">
515
+ <span id="avgMinutes">0</span> min/treino
516
+ </div>
517
+ </div>
518
+ </div>
519
+
520
+ <div class="stat-detail-card">
521
+ <div class="stat-detail-icon">🔥</div>
522
+ <div class="stat-detail-content">
523
+ <div class="stat-detail-number" id="totalCaloriesDetail">0</div>
524
+ <div class="stat-detail-label">Calorias Queimadas</div>
525
+ <div class="stat-detail-sublabel">
526
+ <span id="avgCalories">0</span> kcal/treino
527
+ </div>
528
+ </div>
529
+ </div>
530
+
531
+ <div class="stat-detail-card">
532
+ <div class="stat-detail-icon">📅</div>
533
+ <div class="stat-detail-content">
534
+ <div class="stat-detail-number" id="daysActiveDetail">0</div>
535
+ <div class="stat-detail-label">Dias Ativos</div>
536
+ <div class="stat-detail-sublabel">
537
+ Sequência: <span id="currentStreak">0</span> dias
538
+ </div>
539
+ </div>
540
+ </div>
541
+
542
+ <div class="stat-detail-card">
543
+ <div class="stat-detail-icon">💧</div>
544
+ <div class="stat-detail-content">
545
+ <div class="stat-detail-number" id="totalWaterGlasses">0</div>
546
+ <div class="stat-detail-label">Copos de Água</div>
547
+ <div class="stat-detail-sublabel">
548
+ <span id="waterStreak">0</span> dias 8 copos
549
+ </div>
550
+ </div>
551
+ </div>
552
+
553
+ <div class="stat-detail-card">
554
+ <div class="stat-detail-icon">🏆</div>
555
+ <div class="stat-detail-content">
556
+ <div class="stat-detail-number" id="achievementsUnlocked">0</div>
557
+ <div class="stat-detail-label">Conquistas</div>
558
+ <div class="stat-detail-sublabel">
559
+ de <span id="totalAchievements">12</span> possíveis
560
+ </div>
561
+ </div>
562
+ </div>
563
+ </div>
564
+ </div>
565
+
566
+ <!-- Weekly Activity Chart -->
567
+ <div class="activity-chart-section">
568
+ <h3>Atividade Semanal 📈</h3>
569
+ <div class="weekly-chart">
570
+ <div class="chart-bars" id="weeklyChart">
571
+ <!-- Chart will be rendered here -->
572
+ </div>
573
+ </div>
574
+ </div>
575
+
576
+ <!-- Achievements -->
577
+ <div class="achievements-section">
578
+ <h3>Conquistas 🏆</h3>
579
+ <div class="achievements-grid" id="achievementsGrid">
580
+ <!-- Achievements will be populated here -->
581
+ </div>
582
+ </div>
583
+
584
+ <!-- Personal Records -->
585
+ <div class="records-section">
586
+ <h3>Recordes Pessoais 🌟</h3>
587
+ <div class="records-list">
588
+ <div class="record-item">
589
+ <div class="record-icon">🔥</div>
590
+ <div class="record-content">
591
+ <div class="record-label">Maior Sequência</div>
592
+ <div class="record-value" id="longestStreak">0 dias</div>
593
+ </div>
594
+ </div>
595
+ <div class="record-item">
596
+ <div class="record-icon">⏱️</div>
597
+ <div class="record-content">
598
+ <div class="record-label">Treino Mais Longo</div>
599
+ <div class="record-value" id="longestWorkout">0 min</div>
600
+ </div>
601
+ </div>
602
+ <div class="record-item">
603
+ <div class="record-icon">💪</div>
604
+ <div class="record-content">
605
+ <div class="record-label">Categoria Favorita</div>
606
+ <div class="record-value" id="favoriteCategory">--</div>
607
+ </div>
608
+ </div>
609
+ <div class="record-item">
610
+ <div class="record-icon">📅</div>
611
+ <div class="record-content">
612
+ <div class="record-label">Membro Desde</div>
613
+ <div class="record-value" id="memberSince">--</div>
614
+ </div>
615
+ </div>
616
+ </div>
617
+ </div>
618
+ </section>
619
+ </main>
620
+
621
+ <!-- Bottom Navigation -->
622
+ <nav class="bottom-nav">
623
+ <button class="nav-item active" data-nav="home">
624
+ <span class="nav-icon">🏠</span>
625
+ <span class="nav-label">Início</span>
626
+ </button>
627
+ <button class="nav-item" data-nav="workouts">
628
+ <span class="nav-icon">💪</span>
629
+ <span class="nav-label">Treinar</span>
630
+ </button>
631
+ <button class="nav-item" data-nav="nutrition">
632
+ <span class="nav-icon">🥗</span>
633
+ <span class="nav-label">Nutrição</span>
634
+ </button>
635
+ <button class="nav-item" data-nav="progress">
636
+ <span class="nav-icon">📊</span>
637
+ <span class="nav-label">Progresso</span>
638
+ </button>
639
+ </nav>
640
+ </div>
641
+
642
+ <!-- Completion Modal -->
643
+ <div class="modal" id="completionModal">
644
+ <div class="modal-content celebration">
645
+ <div class="celebration-confetti">🎉</div>
646
+ <h2 class="modal-title">Parabéns! 🎊</h2>
647
+ <p class="modal-message" id="completionMessage">Você completou o treino!</p>
648
+ <div class="workout-summary" id="workoutSummary">
649
+ <div class="summary-stat">
650
+ <span class="summary-icon">⏱️</span>
651
+ <span class="summary-value" id="summaryTime">15 min</span>
652
+ </div>
653
+ <div class="summary-stat">
654
+ <span class="summary-icon">🔥</span>
655
+ <span class="summary-value" id="summaryCalories">120 kcal</span>
656
+ </div>
657
+ <div class="summary-stat">
658
+ <span class="summary-icon">💪</span>
659
+ <span class="summary-value" id="summaryExercises">8 exercícios</span>
660
+ </div>
661
+ </div>
662
+ <div class="workout-details" id="workoutDetails" style="margin-top: var(--spacing-md); padding: var(--spacing-md); background: var(--bg-light); border-radius: var(--radius-md); text-align: left; font-size: 0.9rem; color: var(--text-secondary);"></div>
663
+ <button class="btn-modal-primary" id="closeCompletionModal">Continuar ✨</button>
664
+ </div>
665
+ </div>
666
+
667
+ <!-- Weight Update Modal -->
668
+ <div class="modal" id="weightModal">
669
+ <div class="modal-content">
670
+ <h2 class="modal-title">Atualizar Peso ⚖️</h2>
671
+ <div class="weight-input-group">
672
+ <label for="weightInput">Seu peso atual (kg)</label>
673
+ <input type="number" id="weightInput" step="0.1" min="30" max="200" placeholder="Ex: 65.5">
674
+ </div>
675
+ <div class="weight-input-group">
676
+ <label for="goalWeightInput">Seu peso meta (kg)</label>
677
+ <input type="number" id="goalWeightInput" step="0.1" min="30" max="200" placeholder="Ex: 60.0">
678
+ </div>
679
+ <div class="modal-actions">
680
+ <button class="btn-modal-secondary" id="cancelWeightBtn">Cancelar</button>
681
+ <button class="btn-modal-primary" id="saveWeightBtn">Salvar 💖</button>
682
+ </div>
683
+ </div>
684
+ </div>
685
+
686
+ <!-- Settings Toggle (Sound Control) -->
687
+ <div class="settings-fab" id="settingsFab">
688
+ <button class="fab-settings" id="toggleSound">
689
+ <span class="fab-icon" id="soundIcon">🔊</span>
690
+ </button>
691
+ </div>
692
+
693
+ <!-- Performance: Critical path optimization -->
694
+ <!-- Lazy video loading (high priority) -->
695
+ <link rel="preload" href="lazy-video.js" as="script">
696
+ <script src="lazy-video.js" defer></script>
697
+
698
+ <!-- Performance monitoring (development only) -->
699
+ <script>
700
+ // Only load performance monitor in dev mode
701
+ if (location.hostname === 'localhost' || location.search.includes('debug=true')) {
702
+ const script = document.createElement('script');
703
+ script.src = 'performance-monitor.js';
704
+ script.defer = true;
705
+ document.head.appendChild(script);
706
+ }
707
+ </script>
708
+
709
+ <!-- Performance: Defer non-critical JavaScript -->
710
+ <!-- Personal Plan Modal -->
711
+ <div class="modal" id="planModal">
712
+ <div class="modal-content plan-modal-content">
713
+ <button class="modal-close" onclick="app.closeModal('planModal')">×</button>
714
+ <div id="planModalContent">
715
+ <h2 class="modal-title">🎯 Seu Plano Completo</h2>
716
+
717
+ <div class="profile-info" id="profileInfo"></div>
718
+
719
+ <div class="plan-section">
720
+ <h3>🍽️ Plano Nutricional</h3>
721
+ <div id="nutritionPlan"></div>
722
+ </div>
723
+
724
+ <div class="plan-section">
725
+ <h3>💪 Plano de Treino</h3>
726
+ <div id="workoutPlan"></div>
727
+ </div>
728
+
729
+ <div class="plan-section">
730
+ <h3>📅 Linha do Tempo</h3>
731
+ <div id="timelinePlan"></div>
732
+ </div>
733
+
734
+ <div class="plan-section">
735
+ <h3>💡 Dicas Personalizadas</h3>
736
+ <div id="tipsPlan"></div>
737
+ </div>
738
+
739
+ <button class="btn-edit-profile" id="editProfile">✏️ Editar Perfil</button>
740
+ </div>
741
+ </div>
742
+ </div>
743
+
744
+ <script src="app.js" defer></script>
745
+ </body>
746
+ </html>
public/lazy-video.js ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Lazy Video Loader
3
+ *
4
+ * Optimized video loading system that:
5
+ * 1. Only loads videos when they're about to be viewed
6
+ * 2. Uses Intersection Observer for efficient detection
7
+ * 3. Provides loading states and error handling
8
+ * 4. Caches loaded videos for instant replay
9
+ */
10
+
11
+ class LazyVideoLoader {
12
+ constructor(options = {}) {
13
+ this.options = {
14
+ rootMargin: '50px',
15
+ threshold: 0.1,
16
+ cacheSize: 5, // Keep 5 videos cached
17
+ ...options
18
+ };
19
+
20
+ this.videoCache = new Map();
21
+ this.loadingQueue = new Set();
22
+ this.observer = null;
23
+
24
+ this.init();
25
+ }
26
+
27
+ init() {
28
+ // Create Intersection Observer
29
+ if ('IntersectionObserver' in window) {
30
+ this.observer = new IntersectionObserver(
31
+ (entries) => this.handleIntersection(entries),
32
+ {
33
+ rootMargin: this.options.rootMargin,
34
+ threshold: this.options.threshold
35
+ }
36
+ );
37
+ }
38
+ }
39
+
40
+ handleIntersection(entries) {
41
+ entries.forEach(entry => {
42
+ if (entry.isIntersecting) {
43
+ const video = entry.target;
44
+ this.loadVideo(video);
45
+ this.observer.unobserve(video);
46
+ }
47
+ });
48
+ }
49
+
50
+ observe(videoElement) {
51
+ if (this.observer) {
52
+ this.observer.observe(videoElement);
53
+ } else {
54
+ // Fallback: load immediately if IntersectionObserver not supported
55
+ this.loadVideo(videoElement);
56
+ }
57
+ }
58
+
59
+ async loadVideo(videoElement) {
60
+ const src = videoElement.dataset.src;
61
+
62
+ if (!src || this.loadingQueue.has(src)) {
63
+ return;
64
+ }
65
+
66
+ // Check cache first
67
+ if (this.videoCache.has(src)) {
68
+ videoElement.src = this.videoCache.get(src);
69
+ videoElement.load();
70
+ return;
71
+ }
72
+
73
+ this.loadingQueue.add(src);
74
+
75
+ try {
76
+ // Show loading state
77
+ this.showLoading(videoElement);
78
+
79
+ // Preload the video
80
+ const blob = await this.fetchVideo(src);
81
+ const url = URL.createObjectURL(blob);
82
+
83
+ // Cache the video
84
+ this.cacheVideo(src, url);
85
+
86
+ // Set video source
87
+ videoElement.src = url;
88
+ videoElement.load();
89
+
90
+ // Hide loading state
91
+ this.hideLoading(videoElement);
92
+
93
+ } catch (error) {
94
+ console.error('Error loading video:', error);
95
+ this.showError(videoElement);
96
+ } finally {
97
+ this.loadingQueue.delete(src);
98
+ }
99
+ }
100
+
101
+ async fetchVideo(src) {
102
+ const response = await fetch(src);
103
+
104
+ if (!response.ok) {
105
+ throw new Error(`Failed to load video: ${response.status}`);
106
+ }
107
+
108
+ return await response.blob();
109
+ }
110
+
111
+ cacheVideo(src, url) {
112
+ // Implement LRU cache
113
+ if (this.videoCache.size >= this.options.cacheSize) {
114
+ // Remove oldest cached video
115
+ const firstKey = this.videoCache.keys().next().value;
116
+ const oldUrl = this.videoCache.get(firstKey);
117
+ URL.revokeObjectURL(oldUrl); // Free memory
118
+ this.videoCache.delete(firstKey);
119
+ }
120
+
121
+ this.videoCache.set(src, url);
122
+ }
123
+
124
+ showLoading(videoElement) {
125
+ const container = videoElement.parentElement;
126
+ if (container) {
127
+ container.classList.add('video-loading');
128
+
129
+ // Add loading spinner if not exists
130
+ if (!container.querySelector('.video-loader')) {
131
+ const loader = document.createElement('div');
132
+ loader.className = 'video-loader';
133
+ loader.innerHTML = '<div class="spinner"></div>';
134
+ container.appendChild(loader);
135
+ }
136
+ }
137
+ }
138
+
139
+ hideLoading(videoElement) {
140
+ const container = videoElement.parentElement;
141
+ if (container) {
142
+ container.classList.remove('video-loading');
143
+ const loader = container.querySelector('.video-loader');
144
+ if (loader) {
145
+ loader.remove();
146
+ }
147
+ }
148
+ }
149
+
150
+ showError(videoElement) {
151
+ const container = videoElement.parentElement;
152
+ if (container) {
153
+ container.classList.add('video-error');
154
+ container.classList.remove('video-loading');
155
+
156
+ const loader = container.querySelector('.video-loader');
157
+ if (loader) {
158
+ loader.remove();
159
+ }
160
+ }
161
+ }
162
+
163
+ preloadVideo(src) {
164
+ // Preload a video in the background
165
+ if (!this.videoCache.has(src) && !this.loadingQueue.has(src)) {
166
+ this.loadingQueue.add(src);
167
+
168
+ this.fetchVideo(src)
169
+ .then(blob => {
170
+ const url = URL.createObjectURL(blob);
171
+ this.cacheVideo(src, url);
172
+ })
173
+ .catch(err => console.error('Preload failed:', err))
174
+ .finally(() => this.loadingQueue.delete(src));
175
+ }
176
+ }
177
+
178
+ clearCache() {
179
+ // Clear all cached videos
180
+ this.videoCache.forEach(url => URL.revokeObjectURL(url));
181
+ this.videoCache.clear();
182
+ }
183
+
184
+ destroy() {
185
+ if (this.observer) {
186
+ this.observer.disconnect();
187
+ }
188
+ this.clearCache();
189
+ }
190
+ }
191
+
192
+ // Global instance
193
+ if (typeof window !== 'undefined') {
194
+ window.lazyVideoLoader = new LazyVideoLoader({
195
+ rootMargin: '100px', // Start loading when video is 100px from viewport
196
+ threshold: 0.1,
197
+ cacheSize: 5
198
+ });
199
+ }
200
+
201
+ // Export for module systems
202
+ if (typeof module !== 'undefined' && module.exports) {
203
+ module.exports = LazyVideoLoader;
204
+ }
205
+
public/manifest.json ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "Fitness App - Sua Transformação",
3
+ "short_name": "Fitness App",
4
+ "description": "Aplicativo completo de fitness, nutrição e bem-estar. Treinos, yoga, meditação e muito mais!",
5
+ "start_url": "/?source=pwa",
6
+ "display": "standalone",
7
+ "background_color": "#FFF5F8",
8
+ "theme_color": "#FF6B9D",
9
+ "orientation": "portrait-primary",
10
+ "scope": "/",
11
+ "lang": "pt-BR",
12
+ "dir": "ltr",
13
+ "prefer_related_applications": false,
14
+ "categories": ["health", "fitness", "lifestyle", "wellness"],
15
+ "icons": [
16
+ {
17
+ "src": "icons/icon-72x72.svg",
18
+ "sizes": "72x72",
19
+ "type": "image/svg+xml",
20
+ "purpose": "maskable any"
21
+ },
22
+ {
23
+ "src": "icons/icon-96x96.svg",
24
+ "sizes": "96x96",
25
+ "type": "image/svg+xml",
26
+ "purpose": "maskable any"
27
+ },
28
+ {
29
+ "src": "icons/icon-128x128.svg",
30
+ "sizes": "128x128",
31
+ "type": "image/svg+xml",
32
+ "purpose": "maskable any"
33
+ },
34
+ {
35
+ "src": "icons/icon-144x144.svg",
36
+ "sizes": "144x144",
37
+ "type": "image/svg+xml",
38
+ "purpose": "maskable any"
39
+ },
40
+ {
41
+ "src": "icons/icon-152x152.svg",
42
+ "sizes": "152x152",
43
+ "type": "image/svg+xml",
44
+ "purpose": "maskable any"
45
+ },
46
+ {
47
+ "src": "icons/icon-192x192.svg",
48
+ "sizes": "192x192",
49
+ "type": "image/svg+xml",
50
+ "purpose": "maskable any"
51
+ },
52
+ {
53
+ "src": "icons/icon-384x384.svg",
54
+ "sizes": "384x384",
55
+ "type": "image/svg+xml",
56
+ "purpose": "maskable any"
57
+ },
58
+ {
59
+ "src": "icons/icon-512x512.svg",
60
+ "sizes": "512x512",
61
+ "type": "image/svg+xml",
62
+ "purpose": "maskable any"
63
+ }
64
+ ],
65
+ "shortcuts": [
66
+ {
67
+ "name": "Dia Atual",
68
+ "short_name": "Hoje",
69
+ "description": "Ver o plano do dia atual",
70
+ "url": "/",
71
+ "icons": [
72
+ {
73
+ "src": "icons/icon-96x96.png",
74
+ "sizes": "96x96"
75
+ }
76
+ ]
77
+ },
78
+ {
79
+ "name": "Cronômetro",
80
+ "short_name": "Timer",
81
+ "description": "Usar cronômetro para treinos",
82
+ "url": "/#timer",
83
+ "icons": [
84
+ {
85
+ "src": "icons/icon-96x96.png",
86
+ "sizes": "96x96"
87
+ }
88
+ ]
89
+ }
90
+ ],
91
+ "screenshots": [
92
+ {
93
+ "src": "screenshots/mobile-1.png",
94
+ "sizes": "375x812",
95
+ "type": "image/png",
96
+ "form_factor": "narrow"
97
+ },
98
+ {
99
+ "src": "screenshots/mobile-2.png",
100
+ "sizes": "375x812",
101
+ "type": "image/png",
102
+ "form_factor": "narrow"
103
+ }
104
+ ]
105
+ }
public/performance-monitor.js ADDED
@@ -0,0 +1,422 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Performance Monitoring Utility
3
+ *
4
+ * Tracks and reports Web Vitals and custom metrics
5
+ * Only loads in development or when explicitly enabled
6
+ */
7
+
8
+ class PerformanceMonitor {
9
+ constructor(options = {}) {
10
+ this.options = {
11
+ enableInProduction: false,
12
+ logToConsole: true,
13
+ sendToAnalytics: false,
14
+ ...options
15
+ };
16
+
17
+ this.metrics = {
18
+ vitals: {},
19
+ custom: {},
20
+ resources: [],
21
+ };
22
+
23
+ // Only initialize if enabled
24
+ if (this.shouldMonitor()) {
25
+ this.init();
26
+ }
27
+ }
28
+
29
+ shouldMonitor() {
30
+ return this.options.enableInProduction ||
31
+ location.hostname === 'localhost' ||
32
+ location.search.includes('debug=true');
33
+ }
34
+
35
+ init() {
36
+ console.log('📊 Performance monitoring enabled');
37
+
38
+ // Track Web Vitals
39
+ this.trackWebVitals();
40
+
41
+ // Track Navigation Timing
42
+ this.trackNavigationTiming();
43
+
44
+ // Track Resource Loading
45
+ this.trackResourceTiming();
46
+
47
+ // Track Custom Metrics
48
+ this.setupCustomMetrics();
49
+
50
+ // Report on page hide
51
+ this.setupReporting();
52
+ }
53
+
54
+ // =======================
55
+ // Web Vitals Tracking
56
+ // =======================
57
+
58
+ trackWebVitals() {
59
+ // Largest Contentful Paint (LCP)
60
+ this.trackLCP();
61
+
62
+ // First Input Delay (FID)
63
+ this.trackFID();
64
+
65
+ // Cumulative Layout Shift (CLS)
66
+ this.trackCLS();
67
+
68
+ // First Contentful Paint (FCP)
69
+ this.trackFCP();
70
+
71
+ // Time to First Byte (TTFB)
72
+ this.trackTTFB();
73
+ }
74
+
75
+ trackLCP() {
76
+ if (!('PerformanceObserver' in window)) return;
77
+
78
+ try {
79
+ const observer = new PerformanceObserver((list) => {
80
+ const entries = list.getEntries();
81
+ const lastEntry = entries[entries.length - 1];
82
+
83
+ this.metrics.vitals.LCP = {
84
+ value: lastEntry.renderTime || lastEntry.loadTime,
85
+ rating: this.rateLCP(lastEntry.renderTime || lastEntry.loadTime)
86
+ };
87
+
88
+ this.log('LCP', this.metrics.vitals.LCP);
89
+ });
90
+
91
+ observer.observe({ entryTypes: ['largest-contentful-paint'] });
92
+ } catch (e) {
93
+ console.error('Error tracking LCP:', e);
94
+ }
95
+ }
96
+
97
+ trackFID() {
98
+ if (!('PerformanceObserver' in window)) return;
99
+
100
+ try {
101
+ const observer = new PerformanceObserver((list) => {
102
+ const entries = list.getEntries();
103
+ entries.forEach(entry => {
104
+ this.metrics.vitals.FID = {
105
+ value: entry.processingStart - entry.startTime,
106
+ rating: this.rateFID(entry.processingStart - entry.startTime)
107
+ };
108
+
109
+ this.log('FID', this.metrics.vitals.FID);
110
+ });
111
+ });
112
+
113
+ observer.observe({ entryTypes: ['first-input'] });
114
+ } catch (e) {
115
+ console.error('Error tracking FID:', e);
116
+ }
117
+ }
118
+
119
+ trackCLS() {
120
+ if (!('PerformanceObserver' in window)) return;
121
+
122
+ try {
123
+ let clsValue = 0;
124
+
125
+ const observer = new PerformanceObserver((list) => {
126
+ for (const entry of list.getEntries()) {
127
+ if (!entry.hadRecentInput) {
128
+ clsValue += entry.value;
129
+
130
+ this.metrics.vitals.CLS = {
131
+ value: clsValue,
132
+ rating: this.rateCLS(clsValue)
133
+ };
134
+ }
135
+ }
136
+
137
+ this.log('CLS', this.metrics.vitals.CLS);
138
+ });
139
+
140
+ observer.observe({ entryTypes: ['layout-shift'] });
141
+ } catch (e) {
142
+ console.error('Error tracking CLS:', e);
143
+ }
144
+ }
145
+
146
+ trackFCP() {
147
+ if (!('PerformanceObserver' in window)) return;
148
+
149
+ try {
150
+ const observer = new PerformanceObserver((list) => {
151
+ const entries = list.getEntries();
152
+ entries.forEach(entry => {
153
+ if (entry.name === 'first-contentful-paint') {
154
+ this.metrics.vitals.FCP = {
155
+ value: entry.startTime,
156
+ rating: this.rateFCP(entry.startTime)
157
+ };
158
+
159
+ this.log('FCP', this.metrics.vitals.FCP);
160
+ }
161
+ });
162
+ });
163
+
164
+ observer.observe({ entryTypes: ['paint'] });
165
+ } catch (e) {
166
+ console.error('Error tracking FCP:', e);
167
+ }
168
+ }
169
+
170
+ trackTTFB() {
171
+ if (!window.performance || !window.performance.timing) return;
172
+
173
+ window.addEventListener('load', () => {
174
+ const timing = performance.timing;
175
+ const ttfb = timing.responseStart - timing.requestStart;
176
+
177
+ this.metrics.vitals.TTFB = {
178
+ value: ttfb,
179
+ rating: this.rateTTFB(ttfb)
180
+ };
181
+
182
+ this.log('TTFB', this.metrics.vitals.TTFB);
183
+ });
184
+ }
185
+
186
+ // =======================
187
+ // Navigation Timing
188
+ // =======================
189
+
190
+ trackNavigationTiming() {
191
+ window.addEventListener('load', () => {
192
+ if (!performance.timing) return;
193
+
194
+ const timing = performance.timing;
195
+
196
+ this.metrics.navigation = {
197
+ dns: timing.domainLookupEnd - timing.domainLookupStart,
198
+ tcp: timing.connectEnd - timing.connectStart,
199
+ request: timing.responseStart - timing.requestStart,
200
+ response: timing.responseEnd - timing.responseStart,
201
+ domProcessing: timing.domComplete - timing.domLoading,
202
+ domContentLoaded: timing.domContentLoadedEventEnd - timing.navigationStart,
203
+ loadComplete: timing.loadEventEnd - timing.navigationStart
204
+ };
205
+
206
+ this.log('Navigation Timing', this.metrics.navigation);
207
+ });
208
+ }
209
+
210
+ // =======================
211
+ // Resource Timing
212
+ // =======================
213
+
214
+ trackResourceTiming() {
215
+ window.addEventListener('load', () => {
216
+ if (!performance.getEntriesByType) return;
217
+
218
+ const resources = performance.getEntriesByType('resource');
219
+
220
+ const summary = {
221
+ total: resources.length,
222
+ byType: {},
223
+ slow: []
224
+ };
225
+
226
+ resources.forEach(resource => {
227
+ // Group by type
228
+ const type = this.getResourceType(resource.name);
229
+ summary.byType[type] = (summary.byType[type] || 0) + 1;
230
+
231
+ // Track slow resources (> 1s)
232
+ if (resource.duration > 1000) {
233
+ summary.slow.push({
234
+ name: resource.name,
235
+ duration: resource.duration,
236
+ size: resource.transferSize
237
+ });
238
+ }
239
+ });
240
+
241
+ this.metrics.resources = summary;
242
+ this.log('Resources', summary);
243
+ });
244
+ }
245
+
246
+ getResourceType(url) {
247
+ if (url.match(/\.(js|mjs)$/)) return 'script';
248
+ if (url.match(/\.(css)$/)) return 'stylesheet';
249
+ if (url.match(/\.(jpg|jpeg|png|gif|webp|svg)$/)) return 'image';
250
+ if (url.match(/\.(mp4|webm)$/)) return 'video';
251
+ if (url.match(/\.(mp3|ogg|wav)$/)) return 'audio';
252
+ if (url.match(/\.(woff|woff2|ttf|eot)$/)) return 'font';
253
+ return 'other';
254
+ }
255
+
256
+ // =======================
257
+ // Custom Metrics
258
+ // =======================
259
+
260
+ setupCustomMetrics() {
261
+ // Track app initialization time
262
+ this.mark('app-init-start');
263
+
264
+ // Track video load times
265
+ window.addEventListener('video-loaded', (e) => {
266
+ this.recordCustomMetric('video-load-time', e.detail.duration);
267
+ });
268
+
269
+ // Track workout completion
270
+ window.addEventListener('workout-completed', (e) => {
271
+ this.recordCustomMetric('workout-duration', e.detail.duration);
272
+ });
273
+ }
274
+
275
+ mark(name) {
276
+ if (performance.mark) {
277
+ performance.mark(name);
278
+ }
279
+ }
280
+
281
+ measure(name, startMark, endMark) {
282
+ if (performance.measure) {
283
+ try {
284
+ performance.measure(name, startMark, endMark);
285
+ const measure = performance.getEntriesByName(name)[0];
286
+ this.recordCustomMetric(name, measure.duration);
287
+ } catch (e) {
288
+ console.error('Error measuring:', e);
289
+ }
290
+ }
291
+ }
292
+
293
+ recordCustomMetric(name, value) {
294
+ this.metrics.custom[name] = value;
295
+ this.log(`Custom: ${name}`, value);
296
+ }
297
+
298
+ // =======================
299
+ // Rating Functions
300
+ // =======================
301
+
302
+ rateLCP(value) {
303
+ if (value <= 2500) return 'good';
304
+ if (value <= 4000) return 'needs-improvement';
305
+ return 'poor';
306
+ }
307
+
308
+ rateFID(value) {
309
+ if (value <= 100) return 'good';
310
+ if (value <= 300) return 'needs-improvement';
311
+ return 'poor';
312
+ }
313
+
314
+ rateCLS(value) {
315
+ if (value <= 0.1) return 'good';
316
+ if (value <= 0.25) return 'needs-improvement';
317
+ return 'poor';
318
+ }
319
+
320
+ rateFCP(value) {
321
+ if (value <= 1800) return 'good';
322
+ if (value <= 3000) return 'needs-improvement';
323
+ return 'poor';
324
+ }
325
+
326
+ rateTTFB(value) {
327
+ if (value <= 800) return 'good';
328
+ if (value <= 1800) return 'needs-improvement';
329
+ return 'poor';
330
+ }
331
+
332
+ // =======================
333
+ // Reporting
334
+ // =======================
335
+
336
+ setupReporting() {
337
+ // Report on page hide (works better than unload)
338
+ document.addEventListener('visibilitychange', () => {
339
+ if (document.visibilityState === 'hidden') {
340
+ this.report();
341
+ }
342
+ });
343
+ }
344
+
345
+ report() {
346
+ const report = {
347
+ timestamp: new Date().toISOString(),
348
+ url: location.href,
349
+ userAgent: navigator.userAgent,
350
+ connection: this.getConnectionInfo(),
351
+ vitals: this.metrics.vitals,
352
+ navigation: this.metrics.navigation,
353
+ resources: this.metrics.resources,
354
+ custom: this.metrics.custom
355
+ };
356
+
357
+ if (this.options.logToConsole) {
358
+ console.table(this.metrics.vitals);
359
+ console.log('📊 Full Performance Report:', report);
360
+ }
361
+
362
+ if (this.options.sendToAnalytics) {
363
+ this.sendToAnalytics(report);
364
+ }
365
+
366
+ return report;
367
+ }
368
+
369
+ getConnectionInfo() {
370
+ if (!navigator.connection) return null;
371
+
372
+ return {
373
+ effectiveType: navigator.connection.effectiveType,
374
+ downlink: navigator.connection.downlink,
375
+ rtt: navigator.connection.rtt,
376
+ saveData: navigator.connection.saveData
377
+ };
378
+ }
379
+
380
+ sendToAnalytics(report) {
381
+ // Send using sendBeacon for reliability
382
+ if (navigator.sendBeacon) {
383
+ const blob = new Blob([JSON.stringify(report)], { type: 'application/json' });
384
+ navigator.sendBeacon('/api/analytics/performance', blob);
385
+ }
386
+ }
387
+
388
+ log(name, data) {
389
+ if (this.options.logToConsole) {
390
+ const emoji = this.getEmoji(name);
391
+ console.log(`${emoji} ${name}:`, data);
392
+ }
393
+ }
394
+
395
+ getEmoji(name) {
396
+ const emojiMap = {
397
+ 'LCP': '🖼️',
398
+ 'FID': '⚡',
399
+ 'CLS': '📐',
400
+ 'FCP': '🎨',
401
+ 'TTFB': '⏱️',
402
+ 'Navigation Timing': '🧭',
403
+ 'Resources': '📦'
404
+ };
405
+ return emojiMap[name] || '📊';
406
+ }
407
+ }
408
+
409
+ // Initialize global instance (only in dev mode by default)
410
+ if (typeof window !== 'undefined') {
411
+ window.performanceMonitor = new PerformanceMonitor({
412
+ enableInProduction: false,
413
+ logToConsole: true,
414
+ sendToAnalytics: false
415
+ });
416
+ }
417
+
418
+ // Export for module systems
419
+ if (typeof module !== 'undefined' && module.exports) {
420
+ module.exports = PerformanceMonitor;
421
+ }
422
+
public/styles.css ADDED
@@ -0,0 +1,2736 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Modern Feminine Fitness App - Complete Redesign */
2
+ /* Performance Optimized - v2.3.0 */
3
+
4
+ :root {
5
+ /* Color Palette */
6
+ --primary: #FF6B9D;
7
+ --primary-dark: #E91E63;
8
+ --secondary: #9C27B0;
9
+ --accent: #FFB6C1;
10
+ --success: #4CAF50;
11
+ --warning: #FF9800;
12
+
13
+ /* Gradients */
14
+ --gradient-primary: linear-gradient(135deg, #FF6B9D 0%, #C2185B 100%);
15
+ --gradient-secondary: linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%);
16
+ --gradient-soft: linear-gradient(135deg, #FFE5EC 0%, #FFF0F5 100%);
17
+ --gradient-hero: linear-gradient(135deg, #FF6B9D 0%, #9C27B0 100%);
18
+
19
+ /* Neutral Colors */
20
+ --white: #FFFFFF;
21
+ --bg-light: #FFF5F8;
22
+ --bg-card: #FFFFFF;
23
+ --text-primary: #2D3748;
24
+ --text-secondary: #718096;
25
+ --border: #FFE5EC;
26
+
27
+ /* Shadows */
28
+ --shadow-sm: 0 2px 8px rgba(255, 107, 157, 0.1);
29
+ --shadow-md: 0 4px 16px rgba(255, 107, 157, 0.15);
30
+ --shadow-lg: 0 8px 24px rgba(255, 107, 157, 0.2);
31
+ --shadow-xl: 0 12px 32px rgba(255, 107, 157, 0.25);
32
+
33
+ /* Spacing */
34
+ --spacing-xs: 4px;
35
+ --spacing-sm: 8px;
36
+ --spacing-md: 16px;
37
+ --spacing-lg: 24px;
38
+ --spacing-xl: 32px;
39
+
40
+ /* Border Radius */
41
+ --radius-sm: 8px;
42
+ --radius-md: 16px;
43
+ --radius-lg: 24px;
44
+ --radius-full: 9999px;
45
+ }
46
+
47
+ * {
48
+ margin: 0;
49
+ padding: 0;
50
+ box-sizing: border-box;
51
+ -webkit-tap-highlight-color: transparent;
52
+ }
53
+
54
+ body {
55
+ font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
56
+ background: var(--bg-light);
57
+ color: var(--text-primary);
58
+ overflow-x: hidden;
59
+ -webkit-font-smoothing: antialiased;
60
+ -moz-osx-font-smoothing: grayscale;
61
+ /* Performance: GPU acceleration for smooth scrolling */
62
+ -webkit-overflow-scrolling: touch;
63
+ /* Performance: Optimize text rendering */
64
+ text-rendering: optimizeLegibility;
65
+ }
66
+
67
+ /* Profile Setup Screen */
68
+ .profile-setup-screen {
69
+ position: fixed;
70
+ top: 0;
71
+ left: 0;
72
+ width: 100%;
73
+ height: 100vh;
74
+ background: var(--bg-light);
75
+ overflow-y: auto;
76
+ z-index: 10000;
77
+ padding: var(--spacing-lg);
78
+ }
79
+
80
+ .profile-setup-content {
81
+ max-width: 500px;
82
+ margin: 0 auto;
83
+ padding: var(--spacing-xl) 0;
84
+ }
85
+
86
+ .setup-title {
87
+ font-size: 2rem;
88
+ font-weight: 700;
89
+ color: var(--text-primary);
90
+ text-align: center;
91
+ margin-bottom: var(--spacing-sm);
92
+ }
93
+
94
+ .setup-subtitle {
95
+ text-align: center;
96
+ color: var(--text-secondary);
97
+ margin-bottom: var(--spacing-xl);
98
+ }
99
+
100
+ .profile-form {
101
+ background: var(--white);
102
+ border-radius: var(--radius-lg);
103
+ padding: var(--spacing-xl);
104
+ box-shadow: var(--shadow-md);
105
+ }
106
+
107
+ .form-group {
108
+ margin-bottom: var(--spacing-lg);
109
+ }
110
+
111
+ .form-group label {
112
+ display: block;
113
+ font-weight: 600;
114
+ color: var(--text-primary);
115
+ margin-bottom: var(--spacing-sm);
116
+ }
117
+
118
+ .form-group input,
119
+ .form-group select {
120
+ width: 100%;
121
+ padding: 12px 16px;
122
+ border: 2px solid var(--border);
123
+ border-radius: var(--radius-md);
124
+ font-size: 1rem;
125
+ font-family: inherit;
126
+ transition: all 0.3s ease;
127
+ }
128
+
129
+ .form-group input:focus,
130
+ .form-group select:focus {
131
+ outline: none;
132
+ border-color: var(--primary);
133
+ box-shadow: 0 0 0 3px rgba(255, 107, 157, 0.1);
134
+ }
135
+
136
+ .form-row {
137
+ display: grid;
138
+ grid-template-columns: 1fr 1fr;
139
+ gap: var(--spacing-md);
140
+ }
141
+
142
+ .photo-upload {
143
+ text-align: center;
144
+ }
145
+
146
+ .photo-preview {
147
+ width: 150px;
148
+ height: 150px;
149
+ margin: 0 auto;
150
+ border-radius: var(--radius-full);
151
+ border: 3px dashed var(--border);
152
+ cursor: pointer;
153
+ transition: all 0.3s ease;
154
+ overflow: hidden;
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ }
159
+
160
+ .photo-preview:hover {
161
+ border-color: var(--primary);
162
+ transform: scale(1.05);
163
+ }
164
+
165
+ .photo-placeholder {
166
+ text-align: center;
167
+ }
168
+
169
+ .photo-icon {
170
+ font-size: 48px;
171
+ display: block;
172
+ margin-bottom: var(--spacing-sm);
173
+ }
174
+
175
+ .photo-text {
176
+ color: var(--text-secondary);
177
+ font-size: 0.9rem;
178
+ }
179
+
180
+ .profile-photo {
181
+ width: 100%;
182
+ height: 100%;
183
+ object-fit: cover;
184
+ }
185
+
186
+ .profile-photo {
187
+ width: 100%;
188
+ height: 100%;
189
+ object-fit: cover;
190
+ }
191
+
192
+ .btn-setup-submit {
193
+ width: 100%;
194
+ padding: 16px;
195
+ background: var(--gradient-primary);
196
+ color: var(--white);
197
+ border: none;
198
+ border-radius: var(--radius-full);
199
+ font-size: 1.1rem;
200
+ font-weight: 600;
201
+ cursor: pointer;
202
+ box-shadow: var(--shadow-md);
203
+ transition: all 0.3s ease;
204
+ margin-top: var(--spacing-lg);
205
+ }
206
+
207
+ .btn-setup-submit:hover {
208
+ box-shadow: var(--shadow-lg);
209
+ transform: translateY(-2px);
210
+ }
211
+
212
+ /* Welcome Screen */
213
+ .welcome-screen {
214
+ position: fixed;
215
+ top: 0;
216
+ left: 0;
217
+ width: 100%;
218
+ height: 100vh;
219
+ background: var(--gradient-hero);
220
+ display: flex;
221
+ align-items: center;
222
+ justify-content: center;
223
+ z-index: 9999;
224
+ animation: fadeIn 0.6s ease;
225
+ /* Performance: GPU acceleration */
226
+ will-change: opacity;
227
+ /* Performance: Contain layout */
228
+ contain: layout style paint;
229
+ }
230
+
231
+ .welcome-content {
232
+ text-align: center;
233
+ color: var(--white);
234
+ padding: var(--spacing-xl);
235
+ }
236
+
237
+ .welcome-logo {
238
+ margin-bottom: var(--spacing-xl);
239
+ }
240
+
241
+ .logo-heart {
242
+ font-size: 80px;
243
+ animation: heartbeat 1.5s ease infinite;
244
+ }
245
+
246
+ @keyframes heartbeat {
247
+ 0%, 100% { transform: scale(1); }
248
+ 50% { transform: scale(1.1); }
249
+ }
250
+
251
+ .welcome-content h1 {
252
+ font-family: 'Playfair Display', serif;
253
+ font-size: 2.5rem;
254
+ font-weight: 700;
255
+ margin-bottom: var(--spacing-sm);
256
+ text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
257
+ }
258
+
259
+ .welcome-content p {
260
+ font-size: 1.1rem;
261
+ opacity: 0.95;
262
+ margin-bottom: var(--spacing-xl);
263
+ }
264
+
265
+ .btn-start-journey {
266
+ background: var(--white);
267
+ color: var(--primary);
268
+ border: none;
269
+ padding: 16px 48px;
270
+ font-size: 1.1rem;
271
+ font-weight: 600;
272
+ border-radius: var(--radius-full);
273
+ cursor: pointer;
274
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
275
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
276
+ }
277
+
278
+ .btn-start-journey:hover {
279
+ transform: translateY(-2px);
280
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
281
+ }
282
+
283
+ .btn-start-journey:active {
284
+ transform: translateY(0);
285
+ }
286
+
287
+ /* App Container */
288
+ .app-container {
289
+ max-width: 480px;
290
+ margin: 0 auto;
291
+ background: var(--bg-light);
292
+ min-height: 100vh;
293
+ min-height: -webkit-fill-available;
294
+ position: relative;
295
+ padding-bottom: 80px;
296
+ padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px));
297
+ overflow-x: hidden;
298
+ }
299
+
300
+ /* Top Bar */
301
+ .top-bar {
302
+ background: var(--gradient-hero);
303
+ padding: var(--spacing-md) var(--spacing-lg);
304
+ display: flex;
305
+ justify-content: space-between;
306
+ align-items: center;
307
+ color: var(--white);
308
+ box-shadow: var(--shadow-md);
309
+ }
310
+
311
+ .user-info {
312
+ display: flex;
313
+ align-items: center;
314
+ gap: var(--spacing-md);
315
+ }
316
+
317
+ .avatar {
318
+ width: 48px;
319
+ height: 48px;
320
+ border-radius: var(--radius-full);
321
+ background: rgba(255, 255, 255, 0.2);
322
+ display: flex;
323
+ align-items: center;
324
+ justify-content: center;
325
+ font-size: 24px;
326
+ border: 2px solid rgba(255, 255, 255, 0.3);
327
+ }
328
+
329
+ .user-text {
330
+ display: flex;
331
+ flex-direction: column;
332
+ }
333
+
334
+ .greeting {
335
+ font-weight: 600;
336
+ font-size: 1rem;
337
+ }
338
+
339
+ .streak {
340
+ font-size: 0.85rem;
341
+ opacity: 0.9;
342
+ }
343
+
344
+ .icon-btn {
345
+ background: rgba(255, 255, 255, 0.2);
346
+ border: none;
347
+ width: 40px;
348
+ height: 40px;
349
+ border-radius: var(--radius-full);
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ cursor: pointer;
354
+ font-size: 20px;
355
+ transition: all 0.3s ease;
356
+ position: relative;
357
+ }
358
+
359
+ .icon-btn:hover {
360
+ background: rgba(255, 255, 255, 0.3);
361
+ transform: scale(1.05);
362
+ }
363
+
364
+ .notification-badge {
365
+ position: absolute;
366
+ top: -2px;
367
+ right: -2px;
368
+ background: #FF3B30;
369
+ color: white;
370
+ border-radius: var(--radius-full);
371
+ width: 18px;
372
+ height: 18px;
373
+ font-size: 0.65rem;
374
+ display: flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ font-weight: 700;
378
+ border: 2px solid var(--white);
379
+ }
380
+
381
+ .user-info:hover {
382
+ opacity: 0.9;
383
+ }
384
+
385
+ /* Main View */
386
+ .main-view {
387
+ padding: var(--spacing-lg);
388
+ }
389
+
390
+ .view {
391
+ display: none;
392
+ animation: slideIn 0.3s ease;
393
+ }
394
+
395
+ .view.active {
396
+ display: block;
397
+ }
398
+
399
+ @keyframes slideIn {
400
+ from {
401
+ opacity: 0;
402
+ transform: translateX(20px);
403
+ }
404
+ to {
405
+ opacity: 1;
406
+ transform: translateX(0);
407
+ }
408
+ }
409
+
410
+ /* Hero Section */
411
+ .hero-section {
412
+ margin-bottom: var(--spacing-xl);
413
+ }
414
+
415
+ .page-title {
416
+ font-family: 'Playfair Display', serif;
417
+ font-size: 2rem;
418
+ font-weight: 700;
419
+ color: var(--text-primary);
420
+ margin-bottom: var(--spacing-lg);
421
+ }
422
+
423
+ .daily-progress {
424
+ background: var(--white);
425
+ border-radius: var(--radius-lg);
426
+ padding: var(--spacing-lg);
427
+ box-shadow: var(--shadow-md);
428
+ }
429
+
430
+ .progress-circle {
431
+ position: relative;
432
+ width: 120px;
433
+ height: 120px;
434
+ margin: 0 auto var(--spacing-lg);
435
+ }
436
+
437
+ .progress-circle svg {
438
+ width: 100%;
439
+ height: 100%;
440
+ transform: rotate(-90deg);
441
+ }
442
+
443
+ .progress-bg {
444
+ fill: none;
445
+ stroke: var(--border);
446
+ stroke-width: 8;
447
+ }
448
+
449
+ .progress-fill {
450
+ fill: none;
451
+ stroke: url(#progressGradient);
452
+ stroke-width: 8;
453
+ stroke-linecap: round;
454
+ stroke-dasharray: 339.292;
455
+ stroke-dashoffset: calc(339.292 - (339.292 * var(--progress)) / 100);
456
+ transition: stroke-dashoffset 0.5s ease;
457
+ }
458
+
459
+ .progress-text {
460
+ position: absolute;
461
+ top: 50%;
462
+ left: 50%;
463
+ transform: translate(-50%, -50%);
464
+ text-align: center;
465
+ }
466
+
467
+ .progress-value {
468
+ display: block;
469
+ font-size: 2rem;
470
+ font-weight: 700;
471
+ background: var(--gradient-primary);
472
+ -webkit-background-clip: text;
473
+ -webkit-text-fill-color: transparent;
474
+ background-clip: text;
475
+ }
476
+
477
+ .progress-label {
478
+ font-size: 0.85rem;
479
+ color: var(--text-secondary);
480
+ }
481
+
482
+ .today-stats {
483
+ display: grid;
484
+ grid-template-columns: repeat(3, 1fr);
485
+ gap: var(--spacing-md);
486
+ }
487
+
488
+ .stat {
489
+ text-align: center;
490
+ padding: var(--spacing-md);
491
+ background: var(--gradient-soft);
492
+ border-radius: var(--radius-md);
493
+ }
494
+
495
+ .stat-icon {
496
+ font-size: 24px;
497
+ display: block;
498
+ margin-bottom: var(--spacing-xs);
499
+ }
500
+
501
+ .stat-value {
502
+ font-size: 1.25rem;
503
+ font-weight: 700;
504
+ color: var(--primary);
505
+ }
506
+
507
+ .stat-label {
508
+ font-size: 0.75rem;
509
+ color: var(--text-secondary);
510
+ }
511
+
512
+ /* Quick Actions */
513
+ .quick-actions {
514
+ margin-bottom: var(--spacing-xl);
515
+ }
516
+
517
+ .section-title {
518
+ font-size: 1.25rem;
519
+ font-weight: 600;
520
+ margin-bottom: var(--spacing-md);
521
+ color: var(--text-primary);
522
+ }
523
+
524
+ .action-cards {
525
+ display: grid;
526
+ grid-template-columns: repeat(2, 1fr);
527
+ gap: var(--spacing-md);
528
+ }
529
+
530
+ .action-card {
531
+ background: var(--white);
532
+ border-radius: var(--radius-lg);
533
+ padding: var(--spacing-lg);
534
+ text-align: center;
535
+ box-shadow: var(--shadow-sm);
536
+ cursor: pointer;
537
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
538
+ border: 2px solid transparent;
539
+ /* Performance: Optimize transforms */
540
+ will-change: transform;
541
+ /* Performance: Create stacking context */
542
+ transform: translateZ(0);
543
+ }
544
+
545
+ .action-card:hover {
546
+ transform: translateY(-4px);
547
+ box-shadow: var(--shadow-lg);
548
+ border-color: var(--primary);
549
+ }
550
+
551
+ .action-card:active {
552
+ transform: translateY(-2px);
553
+ }
554
+
555
+ .action-icon {
556
+ font-size: 48px;
557
+ margin-bottom: var(--spacing-sm);
558
+ filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
559
+ }
560
+
561
+ .action-card h4 {
562
+ font-size: 1rem;
563
+ font-weight: 600;
564
+ color: var(--text-primary);
565
+ margin-bottom: var(--spacing-xs);
566
+ }
567
+
568
+ .action-card p {
569
+ font-size: 0.85rem;
570
+ color: var(--text-secondary);
571
+ }
572
+
573
+ /* Motivation Card */
574
+ .motivation-card {
575
+ background: var(--gradient-hero);
576
+ border-radius: var(--radius-lg);
577
+ padding: var(--spacing-lg);
578
+ text-align: center;
579
+ box-shadow: var(--shadow-md);
580
+ margin-bottom: var(--spacing-lg);
581
+ }
582
+
583
+ .motivation-icon {
584
+ font-size: 32px;
585
+ margin-bottom: var(--spacing-sm);
586
+ }
587
+
588
+ .motivation-text {
589
+ color: var(--white);
590
+ font-size: 1rem;
591
+ line-height: 1.6;
592
+ font-weight: 500;
593
+ }
594
+
595
+ /* Category Grid */
596
+ .category-grid {
597
+ display: grid;
598
+ grid-template-columns: repeat(2, 1fr);
599
+ gap: var(--spacing-md);
600
+ }
601
+
602
+ .category-card {
603
+ background: var(--white);
604
+ border-radius: var(--radius-lg);
605
+ padding: var(--spacing-lg);
606
+ text-align: center;
607
+ box-shadow: var(--shadow-sm);
608
+ cursor: pointer;
609
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
610
+ position: relative;
611
+ overflow: hidden;
612
+ /* Performance: Optimize hover animations */
613
+ will-change: transform;
614
+ transform: translateZ(0);
615
+ /* Performance: Contain layout */
616
+ contain: layout style;
617
+ }
618
+
619
+ .category-card::before {
620
+ content: '';
621
+ position: absolute;
622
+ top: 0;
623
+ left: 0;
624
+ right: 0;
625
+ height: 4px;
626
+ background: var(--gradient-primary);
627
+ transform: scaleX(0);
628
+ transition: transform 0.3s ease;
629
+ }
630
+
631
+ .category-card:hover::before {
632
+ transform: scaleX(1);
633
+ }
634
+
635
+ .category-card:hover {
636
+ transform: translateY(-4px);
637
+ box-shadow: var(--shadow-lg);
638
+ }
639
+
640
+ .category-image {
641
+ font-size: 48px;
642
+ margin-bottom: var(--spacing-sm);
643
+ }
644
+
645
+ .category-card h3 {
646
+ font-size: 1.1rem;
647
+ font-weight: 600;
648
+ color: var(--text-primary);
649
+ margin-bottom: var(--spacing-xs);
650
+ }
651
+
652
+ .category-card p {
653
+ font-size: 0.85rem;
654
+ color: var(--text-secondary);
655
+ margin-bottom: var(--spacing-sm);
656
+ }
657
+
658
+ .category-badge {
659
+ display: inline-block;
660
+ background: var(--gradient-soft);
661
+ color: var(--primary);
662
+ padding: 4px 12px;
663
+ border-radius: var(--radius-full);
664
+ font-size: 0.75rem;
665
+ font-weight: 500;
666
+ }
667
+
668
+ /* View Header */
669
+ .view-header {
670
+ margin-bottom: var(--spacing-lg);
671
+ }
672
+
673
+ .btn-back {
674
+ background: var(--white);
675
+ border: none;
676
+ padding: var(--spacing-sm) var(--spacing-md);
677
+ border-radius: var(--radius-full);
678
+ font-weight: 500;
679
+ color: var(--text-primary);
680
+ cursor: pointer;
681
+ box-shadow: var(--shadow-sm);
682
+ margin-bottom: var(--spacing-md);
683
+ transition: all 0.3s ease;
684
+ }
685
+
686
+ .btn-back:hover {
687
+ box-shadow: var(--shadow-md);
688
+ transform: translateX(-2px);
689
+ }
690
+
691
+ .view-title {
692
+ font-family: 'Playfair Display', serif;
693
+ font-size: 1.75rem;
694
+ font-weight: 700;
695
+ color: var(--text-primary);
696
+ }
697
+
698
+ /* Exercise Card */
699
+ .exercises-container {
700
+ display: flex;
701
+ flex-direction: column;
702
+ gap: var(--spacing-md);
703
+ }
704
+
705
+ .exercise-card {
706
+ background: var(--white);
707
+ border-radius: var(--radius-lg);
708
+ padding: var(--spacing-lg);
709
+ box-shadow: var(--shadow-sm);
710
+ cursor: pointer;
711
+ transition: all 0.3s ease;
712
+ display: flex;
713
+ align-items: center;
714
+ gap: var(--spacing-md);
715
+ }
716
+
717
+ .exercise-card:hover {
718
+ box-shadow: var(--shadow-md);
719
+ transform: translateX(4px);
720
+ }
721
+
722
+ .section-header {
723
+ margin: var(--spacing-xl) 0 var(--spacing-md) 0;
724
+ padding: var(--spacing-sm) var(--spacing-md);
725
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
726
+ border-radius: var(--radius-md);
727
+ box-shadow: var(--shadow-sm);
728
+ }
729
+
730
+ .section-header:first-child {
731
+ margin-top: 0;
732
+ }
733
+
734
+ .section-header h3 {
735
+ color: var(--white);
736
+ font-size: 1.1rem;
737
+ font-weight: 600;
738
+ margin: 0;
739
+ text-align: center;
740
+ }
741
+
742
+ .exercise-emoji {
743
+ font-size: 40px;
744
+ flex-shrink: 0;
745
+ }
746
+
747
+ .exercise-info {
748
+ flex: 1;
749
+ }
750
+
751
+ .exercise-name {
752
+ font-size: 1rem;
753
+ font-weight: 600;
754
+ color: var(--text-primary);
755
+ margin-bottom: var(--spacing-xs);
756
+ }
757
+
758
+ .exercise-details {
759
+ font-size: 0.85rem;
760
+ color: var(--text-secondary);
761
+ }
762
+
763
+ .exercise-arrow {
764
+ font-size: 20px;
765
+ color: var(--primary);
766
+ }
767
+
768
+ /* Workout Session */
769
+ .workout-header {
770
+ background: var(--gradient-hero);
771
+ padding: var(--spacing-lg);
772
+ display: flex;
773
+ justify-content: space-between;
774
+ align-items: center;
775
+ color: var(--white);
776
+ }
777
+
778
+ .btn-close-workout {
779
+ background: rgba(255, 255, 255, 0.2);
780
+ border: none;
781
+ width: 40px;
782
+ height: 40px;
783
+ border-radius: var(--radius-full);
784
+ color: var(--white);
785
+ font-size: 24px;
786
+ cursor: pointer;
787
+ display: flex;
788
+ align-items: center;
789
+ justify-content: center;
790
+ transition: all 0.3s ease;
791
+ }
792
+
793
+ .btn-close-workout:hover {
794
+ background: rgba(255, 255, 255, 0.3);
795
+ }
796
+
797
+ .workout-timer {
798
+ font-size: 1.5rem;
799
+ font-weight: 700;
800
+ font-family: 'Courier New', monospace;
801
+ }
802
+
803
+ .workout-content {
804
+ padding: var(--spacing-xl) var(--spacing-lg);
805
+ }
806
+
807
+ .exercise-display {
808
+ text-align: center;
809
+ }
810
+
811
+ .exercise-name {
812
+ font-size: 1.5rem;
813
+ font-weight: 700;
814
+ color: var(--text-primary);
815
+ margin-bottom: var(--spacing-sm);
816
+ }
817
+
818
+ .exercise-count {
819
+ font-size: 0.9rem;
820
+ color: var(--text-secondary);
821
+ margin-bottom: var(--spacing-lg);
822
+ }
823
+
824
+ .exercise-demo {
825
+ margin: var(--spacing-xl) 0;
826
+ }
827
+
828
+ .demo-placeholder {
829
+ width: 95%;
830
+ max-width: 700px;
831
+ margin: 0 auto;
832
+ background: transparent;
833
+ border-radius: 16px;
834
+ display: flex;
835
+ align-items: center;
836
+ justify-content: center;
837
+ overflow: hidden;
838
+ position: relative;
839
+ }
840
+
841
+ .demo-icon {
842
+ font-size: 80px;
843
+ }
844
+
845
+ .demo-video {
846
+ width: 100%;
847
+ height: auto;
848
+ max-height: 75vh;
849
+ object-fit: contain;
850
+ border-radius: 16px;
851
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
852
+ display: block;
853
+ }
854
+
855
+ .exercise-instructions {
856
+ margin-bottom: var(--spacing-lg);
857
+ }
858
+
859
+ .reps-info {
860
+ font-size: 1.1rem;
861
+ font-weight: 600;
862
+ color: var(--primary);
863
+ margin-bottom: var(--spacing-sm);
864
+ }
865
+
866
+ .rest-info {
867
+ font-size: 0.9rem;
868
+ color: var(--text-secondary);
869
+ }
870
+
871
+ .series-tracker {
872
+ display: flex;
873
+ justify-content: center;
874
+ gap: var(--spacing-sm);
875
+ margin-bottom: var(--spacing-xl);
876
+ }
877
+
878
+ .series-dot {
879
+ width: 12px;
880
+ height: 12px;
881
+ border-radius: var(--radius-full);
882
+ background: var(--border);
883
+ transition: all 0.3s ease;
884
+ }
885
+
886
+ .series-dot.completed {
887
+ background: var(--primary);
888
+ transform: scale(1.2);
889
+ }
890
+
891
+ .workout-controls {
892
+ display: flex;
893
+ gap: var(--spacing-md);
894
+ }
895
+
896
+ .btn-workout-action {
897
+ flex: 1;
898
+ padding: 16px;
899
+ border: none;
900
+ border-radius: var(--radius-full);
901
+ font-size: 1rem;
902
+ font-weight: 600;
903
+ cursor: pointer;
904
+ transition: all 0.3s ease;
905
+ }
906
+
907
+ .btn-workout-action.primary {
908
+ background: var(--gradient-primary);
909
+ color: var(--white);
910
+ box-shadow: var(--shadow-md);
911
+ }
912
+
913
+ .btn-workout-action.primary:hover {
914
+ box-shadow: var(--shadow-lg);
915
+ transform: translateY(-2px);
916
+ }
917
+
918
+ .btn-workout-action.secondary {
919
+ background: var(--white);
920
+ color: var(--text-primary);
921
+ box-shadow: var(--shadow-sm);
922
+ }
923
+
924
+ .workout-progress-bar {
925
+ position: fixed;
926
+ bottom: 80px;
927
+ left: 0;
928
+ right: 0;
929
+ height: 4px;
930
+ background: var(--border);
931
+ max-width: 480px;
932
+ margin: 0 auto;
933
+ }
934
+
935
+ .progress-bar-fill {
936
+ height: 100%;
937
+ background: var(--gradient-primary);
938
+ transition: width 0.3s ease;
939
+ }
940
+
941
+ /* Wellness Grid */
942
+ .wellness-grid {
943
+ display: grid;
944
+ grid-template-columns: repeat(2, 1fr);
945
+ gap: var(--spacing-md);
946
+ }
947
+
948
+ .wellness-card {
949
+ background: var(--white);
950
+ border-radius: var(--radius-lg);
951
+ padding: var(--spacing-lg);
952
+ text-align: center;
953
+ box-shadow: var(--shadow-sm);
954
+ cursor: pointer;
955
+ transition: all 0.3s ease;
956
+ }
957
+
958
+ .wellness-card:hover {
959
+ transform: translateY(-4px);
960
+ box-shadow: var(--shadow-lg);
961
+ }
962
+
963
+ .wellness-icon {
964
+ font-size: 48px;
965
+ margin-bottom: var(--spacing-sm);
966
+ }
967
+
968
+ .wellness-card h3 {
969
+ font-size: 1rem;
970
+ font-weight: 600;
971
+ color: var(--text-primary);
972
+ margin-bottom: var(--spacing-xs);
973
+ }
974
+
975
+ .wellness-card p {
976
+ font-size: 0.85rem;
977
+ color: var(--text-secondary);
978
+ margin-bottom: var(--spacing-sm);
979
+ }
980
+
981
+ .duration {
982
+ display: inline-block;
983
+ background: var(--gradient-soft);
984
+ color: var(--primary);
985
+ padding: 4px 12px;
986
+ border-radius: var(--radius-full);
987
+ font-size: 0.75rem;
988
+ font-weight: 500;
989
+ }
990
+
991
+ /* Nutrition */
992
+ .nutrition-summary {
993
+ background: var(--white);
994
+ border-radius: var(--radius-lg);
995
+ padding: var(--spacing-lg);
996
+ box-shadow: var(--shadow-md);
997
+ margin-bottom: var(--spacing-lg);
998
+ }
999
+
1000
+ .nutrition-summary h3 {
1001
+ font-size: 1.25rem;
1002
+ font-weight: 600;
1003
+ margin-bottom: var(--spacing-lg);
1004
+ text-align: center;
1005
+ }
1006
+
1007
+ .macros-display {
1008
+ display: grid;
1009
+ grid-template-columns: repeat(3, 1fr);
1010
+ gap: var(--spacing-md);
1011
+ margin-bottom: var(--spacing-lg);
1012
+ }
1013
+
1014
+ .macro-item {
1015
+ text-align: center;
1016
+ }
1017
+
1018
+ .macro-circle {
1019
+ width: 80px;
1020
+ height: 80px;
1021
+ border-radius: var(--radius-full);
1022
+ display: flex;
1023
+ align-items: center;
1024
+ justify-content: center;
1025
+ margin: 0 auto var(--spacing-sm);
1026
+ font-weight: 700;
1027
+ color: var(--white);
1028
+ }
1029
+
1030
+ .macro-circle.carbs {
1031
+ background: linear-gradient(135deg, #FFB74D 0%, #FF9800 100%);
1032
+ }
1033
+
1034
+ .macro-circle.protein {
1035
+ background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
1036
+ }
1037
+
1038
+ .macro-circle.fat {
1039
+ background: linear-gradient(135deg, #9C27B0 0%, #7B1FA2 100%);
1040
+ }
1041
+
1042
+ .macro-label {
1043
+ font-size: 0.85rem;
1044
+ color: var(--text-secondary);
1045
+ }
1046
+
1047
+ .calories-total {
1048
+ text-align: center;
1049
+ padding: var(--spacing-md);
1050
+ background: var(--gradient-soft);
1051
+ border-radius: var(--radius-md);
1052
+ }
1053
+
1054
+ .calories-value {
1055
+ display: block;
1056
+ font-size: 2rem;
1057
+ font-weight: 700;
1058
+ background: var(--gradient-primary);
1059
+ -webkit-background-clip: text;
1060
+ -webkit-text-fill-color: transparent;
1061
+ background-clip: text;
1062
+ }
1063
+
1064
+ .calories-label {
1065
+ font-size: 0.9rem;
1066
+ color: var(--text-secondary);
1067
+ }
1068
+
1069
+ /* Water Tracker */
1070
+ .water-tracker {
1071
+ background: var(--white);
1072
+ border-radius: var(--radius-lg);
1073
+ padding: var(--spacing-lg);
1074
+ box-shadow: var(--shadow-md);
1075
+ }
1076
+
1077
+ .water-tracker h3 {
1078
+ font-size: 1.25rem;
1079
+ font-weight: 600;
1080
+ margin-bottom: var(--spacing-md);
1081
+ text-align: center;
1082
+ }
1083
+
1084
+ .water-glasses {
1085
+ display: grid;
1086
+ grid-template-columns: repeat(4, 1fr);
1087
+ gap: var(--spacing-sm);
1088
+ margin-bottom: var(--spacing-md);
1089
+ }
1090
+
1091
+ .glass {
1092
+ aspect-ratio: 1;
1093
+ background: var(--border);
1094
+ border-radius: var(--radius-md);
1095
+ display: flex;
1096
+ align-items: center;
1097
+ justify-content: center;
1098
+ font-size: 28px;
1099
+ cursor: pointer;
1100
+ transition: all 0.3s ease;
1101
+ opacity: 0.3;
1102
+ }
1103
+
1104
+ .glass.filled {
1105
+ background: linear-gradient(135deg, #64B5F6 0%, #2196F3 100%);
1106
+ opacity: 1;
1107
+ transform: scale(1.05);
1108
+ }
1109
+
1110
+ .water-goal {
1111
+ text-align: center;
1112
+ color: var(--text-secondary);
1113
+ font-size: 0.9rem;
1114
+ }
1115
+
1116
+ /* Progress & Achievements */
1117
+ .achievements-section,
1118
+ .stats-section {
1119
+ margin-bottom: var(--spacing-xl);
1120
+ }
1121
+
1122
+ .achievements-section h3,
1123
+ .stats-section h3 {
1124
+ font-size: 1.25rem;
1125
+ font-weight: 600;
1126
+ margin-bottom: var(--spacing-md);
1127
+ }
1128
+
1129
+ /* Plan Summary Section */
1130
+ .plan-summary {
1131
+ margin: var(--spacing-xl) 0;
1132
+ }
1133
+
1134
+ .plan-card {
1135
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1136
+ border-radius: var(--radius-lg);
1137
+ padding: var(--spacing-lg);
1138
+ color: var(--white);
1139
+ box-shadow: var(--shadow-lg);
1140
+ }
1141
+
1142
+ .plan-header {
1143
+ display: flex;
1144
+ justify-content: space-between;
1145
+ align-items: center;
1146
+ margin-bottom: var(--spacing-md);
1147
+ }
1148
+
1149
+ .plan-header h3 {
1150
+ font-size: 1.3rem;
1151
+ font-weight: 700;
1152
+ }
1153
+
1154
+ .btn-view-plan {
1155
+ background: rgba(255, 255, 255, 0.2);
1156
+ color: var(--white);
1157
+ padding: 8px 16px;
1158
+ border: 1px solid rgba(255, 255, 255, 0.3);
1159
+ border-radius: var(--radius-full);
1160
+ font-size: 0.9rem;
1161
+ font-weight: 600;
1162
+ cursor: pointer;
1163
+ transition: all 0.3s ease;
1164
+ }
1165
+
1166
+ .btn-view-plan:hover {
1167
+ background: rgba(255, 255, 255, 0.3);
1168
+ transform: scale(1.05);
1169
+ }
1170
+
1171
+ .plan-quick-stats {
1172
+ display: grid;
1173
+ grid-template-columns: repeat(3, 1fr);
1174
+ gap: var(--spacing-md);
1175
+ margin-bottom: var(--spacing-md);
1176
+ }
1177
+
1178
+ .plan-stat {
1179
+ text-align: center;
1180
+ }
1181
+
1182
+ .plan-label {
1183
+ display: block;
1184
+ font-size: 0.85rem;
1185
+ opacity: 0.9;
1186
+ margin-bottom: 4px;
1187
+ }
1188
+
1189
+ .plan-value {
1190
+ display: block;
1191
+ font-size: 1.1rem;
1192
+ font-weight: 700;
1193
+ }
1194
+
1195
+ .coach-message {
1196
+ background: rgba(255, 255, 255, 0.15);
1197
+ padding: var(--spacing-md);
1198
+ border-radius: var(--radius-md);
1199
+ text-align: center;
1200
+ font-size: 0.95rem;
1201
+ border-left: 4px solid rgba(255, 255, 255, 0.5);
1202
+ }
1203
+
1204
+ /* Plan Modal */
1205
+ .plan-modal-content {
1206
+ max-width: 600px;
1207
+ max-height: 85vh;
1208
+ overflow-y: auto;
1209
+ width: 95%;
1210
+ margin: auto;
1211
+ }
1212
+
1213
+ .profile-info {
1214
+ background: var(--bg-light);
1215
+ padding: var(--spacing-lg);
1216
+ border-radius: var(--radius-md);
1217
+ margin-bottom: var(--spacing-lg);
1218
+ }
1219
+
1220
+ .profile-photo-container {
1221
+ display: flex;
1222
+ align-items: center;
1223
+ gap: var(--spacing-md);
1224
+ margin-bottom: var(--spacing-md);
1225
+ }
1226
+
1227
+ .profile-photo-large {
1228
+ width: 80px;
1229
+ height: 80px;
1230
+ border-radius: var(--radius-full);
1231
+ object-fit: cover;
1232
+ }
1233
+
1234
+ .profile-basic-info {
1235
+ flex: 1;
1236
+ }
1237
+
1238
+ .profile-name {
1239
+ font-size: 1.5rem;
1240
+ font-weight: 700;
1241
+ color: var(--text-primary);
1242
+ margin-bottom: 4px;
1243
+ }
1244
+
1245
+ .profile-metrics {
1246
+ display: grid;
1247
+ grid-template-columns: repeat(2, 1fr);
1248
+ gap: var(--spacing-sm);
1249
+ margin-top: var(--spacing-md);
1250
+ }
1251
+
1252
+ .metric-item {
1253
+ display: flex;
1254
+ justify-content: space-between;
1255
+ padding: 8px;
1256
+ background: var(--white);
1257
+ border-radius: var(--radius-sm);
1258
+ }
1259
+
1260
+ .metric-label {
1261
+ color: var(--text-secondary);
1262
+ font-size: 0.9rem;
1263
+ }
1264
+
1265
+ .metric-value {
1266
+ font-weight: 600;
1267
+ color: var(--text-primary);
1268
+ }
1269
+
1270
+ .plan-section {
1271
+ margin-bottom: var(--spacing-xl);
1272
+ }
1273
+
1274
+ .plan-section h3 {
1275
+ font-size: 1.2rem;
1276
+ font-weight: 700;
1277
+ color: var(--text-primary);
1278
+ margin-bottom: var(--spacing-md);
1279
+ padding-bottom: var(--spacing-sm);
1280
+ border-bottom: 2px solid var(--border);
1281
+ }
1282
+
1283
+ .nutrition-grid {
1284
+ display: grid;
1285
+ grid-template-columns: repeat(2, 1fr);
1286
+ gap: var(--spacing-md);
1287
+ margin-bottom: var(--spacing-md);
1288
+ }
1289
+
1290
+ .nutrition-item {
1291
+ background: var(--bg-light);
1292
+ padding: var(--spacing-md);
1293
+ border-radius: var(--radius-md);
1294
+ text-align: center;
1295
+ }
1296
+
1297
+ .nutrition-label {
1298
+ display: block;
1299
+ font-size: 0.9rem;
1300
+ color: var(--text-secondary);
1301
+ margin-bottom: 4px;
1302
+ }
1303
+
1304
+ .nutrition-value {
1305
+ display: block;
1306
+ font-size: 1.5rem;
1307
+ font-weight: 700;
1308
+ color: var(--primary);
1309
+ }
1310
+
1311
+ .nutrition-unit {
1312
+ font-size: 0.9rem;
1313
+ color: var(--text-secondary);
1314
+ margin-left: 4px;
1315
+ }
1316
+
1317
+ .meal-plan {
1318
+ background: var(--bg-light);
1319
+ padding: var(--spacing-md);
1320
+ border-radius: var(--radius-md);
1321
+ }
1322
+
1323
+ .meal-item {
1324
+ padding: var(--spacing-sm) 0;
1325
+ border-bottom: 1px solid var(--border);
1326
+ }
1327
+
1328
+ .meal-item:last-child {
1329
+ border-bottom: none;
1330
+ }
1331
+
1332
+ .meal-name {
1333
+ font-weight: 600;
1334
+ color: var(--text-primary);
1335
+ margin-bottom: 4px;
1336
+ }
1337
+
1338
+ .meal-description {
1339
+ font-size: 0.9rem;
1340
+ color: var(--text-secondary);
1341
+ }
1342
+
1343
+ .workout-info, .timeline-info {
1344
+ background: var(--bg-light);
1345
+ padding: var(--spacing-md);
1346
+ border-radius: var(--radius-md);
1347
+ }
1348
+
1349
+ .info-row {
1350
+ display: flex;
1351
+ justify-content: space-between;
1352
+ padding: var(--spacing-sm) 0;
1353
+ border-bottom: 1px solid var(--border);
1354
+ }
1355
+
1356
+ .info-row:last-child {
1357
+ border-bottom: none;
1358
+ }
1359
+
1360
+ .info-label {
1361
+ color: var(--text-secondary);
1362
+ }
1363
+
1364
+ .info-value {
1365
+ font-weight: 600;
1366
+ color: var(--text-primary);
1367
+ }
1368
+
1369
+ .milestones {
1370
+ display: grid;
1371
+ gap: var(--spacing-sm);
1372
+ margin-top: var(--spacing-md);
1373
+ }
1374
+
1375
+ .milestone-item {
1376
+ display: flex;
1377
+ align-items: center;
1378
+ gap: var(--spacing-md);
1379
+ padding: var(--spacing-sm);
1380
+ background: var(--bg-light);
1381
+ border-radius: var(--radius-md);
1382
+ }
1383
+
1384
+ .milestone-check {
1385
+ width: 30px;
1386
+ height: 30px;
1387
+ border-radius: var(--radius-full);
1388
+ background: var(--success);
1389
+ color: var(--white);
1390
+ display: flex;
1391
+ align-items: center;
1392
+ justify-content: center;
1393
+ font-size: 1.2rem;
1394
+ }
1395
+
1396
+ .tips-list {
1397
+ display: grid;
1398
+ gap: var(--spacing-sm);
1399
+ }
1400
+
1401
+ .tip-item {
1402
+ background: var(--bg-light);
1403
+ padding: var(--spacing-md);
1404
+ border-radius: var(--radius-md);
1405
+ border-left: 4px solid var(--primary);
1406
+ }
1407
+
1408
+ .btn-edit-profile {
1409
+ width: 100%;
1410
+ padding: 16px;
1411
+ background: var(--gradient-primary);
1412
+ color: var(--white);
1413
+ border: none;
1414
+ border-radius: var(--radius-full);
1415
+ font-size: 1.1rem;
1416
+ font-weight: 600;
1417
+ cursor: pointer;
1418
+ box-shadow: var(--shadow-md);
1419
+ transition: all 0.3s ease;
1420
+ margin-top: var(--spacing-lg);
1421
+ }
1422
+
1423
+ .btn-edit-profile:hover {
1424
+ box-shadow: var(--shadow-lg);
1425
+ transform: translateY(-2px);
1426
+ }
1427
+
1428
+ .achievements-grid {
1429
+ display: grid;
1430
+ grid-template-columns: repeat(3, 1fr);
1431
+ gap: var(--spacing-md);
1432
+ }
1433
+
1434
+ .achievement-card {
1435
+ background: var(--white);
1436
+ border-radius: var(--radius-lg);
1437
+ padding: var(--spacing-md);
1438
+ text-align: center;
1439
+ box-shadow: var(--shadow-sm);
1440
+ }
1441
+
1442
+ .achievement-card {
1443
+ transition: all 0.3s ease;
1444
+ }
1445
+
1446
+ .achievement-card.locked {
1447
+ opacity: 0.4;
1448
+ filter: grayscale(1);
1449
+ }
1450
+
1451
+ .achievement-card:not(.locked):hover {
1452
+ transform: translateY(-4px);
1453
+ box-shadow: var(--shadow-md);
1454
+ }
1455
+
1456
+ .achievement-icon {
1457
+ font-size: 40px;
1458
+ margin-bottom: var(--spacing-xs);
1459
+ display: block;
1460
+ }
1461
+
1462
+ .achievement-name {
1463
+ font-size: 0.75rem;
1464
+ font-weight: 600;
1465
+ color: var(--text-primary);
1466
+ }
1467
+
1468
+ @media (max-width: 480px) {
1469
+ .demo-placeholder {
1470
+ width: 98%;
1471
+ max-width: 100%;
1472
+ }
1473
+
1474
+ .demo-video {
1475
+ width: 100%;
1476
+ height: auto;
1477
+ max-height: 65vh;
1478
+ border-radius: 12px;
1479
+ }
1480
+
1481
+ .achievements-grid {
1482
+ grid-template-columns: repeat(2, 1fr);
1483
+ gap: var(--spacing-sm);
1484
+ }
1485
+
1486
+ .form-row {
1487
+ grid-template-columns: 1fr;
1488
+ }
1489
+
1490
+ .plan-quick-stats {
1491
+ grid-template-columns: 1fr;
1492
+ gap: var(--spacing-sm);
1493
+ }
1494
+
1495
+ .profile-metrics {
1496
+ grid-template-columns: 1fr;
1497
+ }
1498
+
1499
+ .nutrition-grid {
1500
+ grid-template-columns: 1fr;
1501
+ }
1502
+
1503
+ .achievement-card {
1504
+ padding: var(--spacing-sm);
1505
+ }
1506
+
1507
+ .achievement-icon {
1508
+ font-size: 32px;
1509
+ }
1510
+
1511
+ .achievement-name {
1512
+ font-size: 0.7rem;
1513
+ }
1514
+ }
1515
+
1516
+ .stats-cards {
1517
+ display: grid;
1518
+ grid-template-columns: repeat(2, 1fr);
1519
+ gap: var(--spacing-md);
1520
+ }
1521
+
1522
+ .stat-card {
1523
+ background: var(--white);
1524
+ border-radius: var(--radius-lg);
1525
+ padding: var(--spacing-lg);
1526
+ text-align: center;
1527
+ box-shadow: var(--shadow-sm);
1528
+ }
1529
+
1530
+ .stat-card .stat-icon {
1531
+ font-size: 32px;
1532
+ margin-bottom: var(--spacing-sm);
1533
+ }
1534
+
1535
+ .stat-number {
1536
+ font-size: 1.75rem;
1537
+ font-weight: 700;
1538
+ background: var(--gradient-primary);
1539
+ -webkit-background-clip: text;
1540
+ -webkit-text-fill-color: transparent;
1541
+ background-clip: text;
1542
+ }
1543
+
1544
+ .stat-label {
1545
+ font-size: 0.85rem;
1546
+ color: var(--text-secondary);
1547
+ }
1548
+
1549
+ /* Bottom Navigation */
1550
+ .bottom-nav {
1551
+ position: fixed;
1552
+ bottom: 0;
1553
+ left: 0;
1554
+ right: 0;
1555
+ max-width: 480px;
1556
+ margin: 0 auto;
1557
+ background: var(--white);
1558
+ box-shadow: 0 -2px 16px rgba(0, 0, 0, 0.1);
1559
+ display: grid;
1560
+ grid-template-columns: repeat(4, 1fr);
1561
+ padding: var(--spacing-sm) 0;
1562
+ z-index: 100;
1563
+ }
1564
+
1565
+ .nav-item {
1566
+ background: none;
1567
+ border: none;
1568
+ padding: var(--spacing-sm);
1569
+ display: flex;
1570
+ flex-direction: column;
1571
+ align-items: center;
1572
+ gap: 4px;
1573
+ cursor: pointer;
1574
+ color: var(--text-secondary);
1575
+ transition: all 0.3s ease;
1576
+ }
1577
+
1578
+ .nav-item.active {
1579
+ color: var(--primary);
1580
+ }
1581
+
1582
+ .nav-icon {
1583
+ font-size: 24px;
1584
+ transition: transform 0.3s ease;
1585
+ }
1586
+
1587
+ .nav-item.active .nav-icon {
1588
+ transform: scale(1.1);
1589
+ }
1590
+
1591
+ .nav-label {
1592
+ font-size: 0.7rem;
1593
+ font-weight: 500;
1594
+ }
1595
+
1596
+ /* Modal */
1597
+ .modal {
1598
+ display: none;
1599
+ position: fixed;
1600
+ top: 0;
1601
+ left: 0;
1602
+ right: 0;
1603
+ bottom: 0;
1604
+ background: rgba(0, 0, 0, 0.5);
1605
+ backdrop-filter: blur(4px);
1606
+ z-index: 1000;
1607
+ align-items: center;
1608
+ justify-content: center;
1609
+ animation: fadeIn 0.3s ease;
1610
+ }
1611
+
1612
+ .modal.active {
1613
+ display: flex;
1614
+ }
1615
+
1616
+ .modal-content {
1617
+ background: var(--white);
1618
+ border-radius: var(--radius-lg);
1619
+ padding: var(--spacing-xl);
1620
+ max-width: 90%;
1621
+ max-height: 90vh;
1622
+ width: 360px;
1623
+ overflow-y: auto;
1624
+ box-shadow: var(--shadow-xl);
1625
+ text-align: center;
1626
+ animation: scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1627
+ }
1628
+
1629
+ .plan-modal-content {
1630
+ background: var(--white);
1631
+ border-radius: var(--radius-lg);
1632
+ padding: var(--spacing-xl);
1633
+ max-width: 90%;
1634
+ max-height: 90vh;
1635
+ width: 500px;
1636
+ overflow-y: auto;
1637
+ box-shadow: var(--shadow-xl);
1638
+ text-align: left;
1639
+ animation: scaleIn 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1640
+ position: relative;
1641
+ }
1642
+
1643
+ @keyframes scaleIn {
1644
+ from {
1645
+ opacity: 0;
1646
+ transform: scale(0.9);
1647
+ }
1648
+ to {
1649
+ opacity: 1;
1650
+ transform: scale(1);
1651
+ }
1652
+ }
1653
+
1654
+ .celebration-confetti {
1655
+ font-size: 64px;
1656
+ margin-bottom: var(--spacing-md);
1657
+ animation: bounce 0.6s ease;
1658
+ }
1659
+
1660
+ @keyframes bounce {
1661
+ 0%, 100% { transform: translateY(0); }
1662
+ 50% { transform: translateY(-20px); }
1663
+ }
1664
+
1665
+ .modal-title {
1666
+ font-size: 1.75rem;
1667
+ font-weight: 700;
1668
+ color: var(--text-primary);
1669
+ margin-bottom: var(--spacing-md);
1670
+ }
1671
+
1672
+ .modal-message {
1673
+ font-size: 1rem;
1674
+ color: var(--text-secondary);
1675
+ margin-bottom: var(--spacing-lg);
1676
+ }
1677
+
1678
+ .workout-summary {
1679
+ display: flex;
1680
+ justify-content: center;
1681
+ gap: var(--spacing-lg);
1682
+ margin-bottom: var(--spacing-lg);
1683
+ }
1684
+
1685
+ .summary-stat {
1686
+ display: flex;
1687
+ align-items: center;
1688
+ gap: var(--spacing-sm);
1689
+ }
1690
+
1691
+ .summary-icon {
1692
+ font-size: 24px;
1693
+ }
1694
+
1695
+ .summary-value {
1696
+ font-weight: 600;
1697
+ color: var(--primary);
1698
+ }
1699
+
1700
+ .btn-modal-primary {
1701
+ background: var(--gradient-primary);
1702
+ color: var(--white);
1703
+ border: none;
1704
+ padding: 16px 48px;
1705
+ border-radius: var(--radius-full);
1706
+ font-size: 1rem;
1707
+ font-weight: 600;
1708
+ cursor: pointer;
1709
+ box-shadow: var(--shadow-md);
1710
+ transition: all 0.3s ease;
1711
+ }
1712
+
1713
+ .btn-modal-primary:hover {
1714
+ box-shadow: var(--shadow-lg);
1715
+ transform: translateY(-2px);
1716
+ }
1717
+
1718
+ /* Weight Tracking */
1719
+ .weight-tracking-section {
1720
+ margin-bottom: var(--spacing-xl);
1721
+ }
1722
+
1723
+ /* Weekly Activity */
1724
+ .weekly-activity-section {
1725
+ margin-bottom: var(--spacing-xl);
1726
+ }
1727
+
1728
+ .weekly-activity-grid {
1729
+ display: grid;
1730
+ grid-template-columns: repeat(7, 1fr);
1731
+ gap: var(--spacing-xs);
1732
+ padding: var(--spacing-md);
1733
+ background: var(--white);
1734
+ border-radius: var(--radius-lg);
1735
+ box-shadow: var(--shadow-sm);
1736
+ }
1737
+
1738
+ .weekly-day {
1739
+ text-align: center;
1740
+ padding: var(--spacing-sm);
1741
+ border-radius: var(--radius-md);
1742
+ background: var(--bg-light);
1743
+ transition: all 0.3s ease;
1744
+ }
1745
+
1746
+ .weekly-day.active {
1747
+ background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
1748
+ color: var(--white);
1749
+ box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
1750
+ }
1751
+
1752
+ .weekly-day.today {
1753
+ border: 2px solid var(--primary);
1754
+ }
1755
+
1756
+ .weekly-day-name {
1757
+ font-size: 0.7rem;
1758
+ font-weight: 600;
1759
+ text-transform: uppercase;
1760
+ margin-bottom: 4px;
1761
+ opacity: 0.7;
1762
+ }
1763
+
1764
+ .weekly-day-number {
1765
+ font-size: 1.1rem;
1766
+ font-weight: 700;
1767
+ margin-bottom: 4px;
1768
+ }
1769
+
1770
+ .weekly-day-workouts {
1771
+ font-size: 0.65rem;
1772
+ opacity: 0.8;
1773
+ }
1774
+
1775
+ /* Exercício Completado nas últimas 24h */
1776
+ .exercise-card.completed-24h {
1777
+ background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%);
1778
+ border-left: 4px solid #4CAF50;
1779
+ position: relative;
1780
+ }
1781
+
1782
+ .exercise-card.completed-24h::after {
1783
+ content: '✓';
1784
+ position: absolute;
1785
+ top: 8px;
1786
+ right: 8px;
1787
+ width: 24px;
1788
+ height: 24px;
1789
+ background: #4CAF50;
1790
+ color: white;
1791
+ border-radius: var(--radius-full);
1792
+ display: flex;
1793
+ align-items: center;
1794
+ justify-content: center;
1795
+ font-weight: 700;
1796
+ font-size: 14px;
1797
+ box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
1798
+ }
1799
+
1800
+ .weight-tracking-section h3 {
1801
+ font-size: 1.25rem;
1802
+ font-weight: 600;
1803
+ margin-bottom: var(--spacing-md);
1804
+ }
1805
+
1806
+ .weight-card {
1807
+ background: var(--white);
1808
+ border-radius: var(--radius-lg);
1809
+ padding: var(--spacing-lg);
1810
+ box-shadow: var(--shadow-md);
1811
+ }
1812
+
1813
+ .weight-current {
1814
+ text-align: center;
1815
+ margin-bottom: var(--spacing-lg);
1816
+ }
1817
+
1818
+ .weight-label {
1819
+ font-size: 0.9rem;
1820
+ color: var(--text-secondary);
1821
+ margin-bottom: var(--spacing-xs);
1822
+ }
1823
+
1824
+ .weight-value {
1825
+ font-size: 3rem;
1826
+ font-weight: 700;
1827
+ background: var(--gradient-primary);
1828
+ -webkit-background-clip: text;
1829
+ -webkit-text-fill-color: transparent;
1830
+ background-clip: text;
1831
+ margin-bottom: var(--spacing-md);
1832
+ }
1833
+
1834
+ .btn-update-weight {
1835
+ background: var(--gradient-primary);
1836
+ color: var(--white);
1837
+ border: none;
1838
+ padding: 12px 32px;
1839
+ border-radius: var(--radius-full);
1840
+ font-weight: 600;
1841
+ cursor: pointer;
1842
+ box-shadow: var(--shadow-sm);
1843
+ transition: all 0.3s ease;
1844
+ }
1845
+
1846
+ .btn-update-weight:hover {
1847
+ box-shadow: var(--shadow-md);
1848
+ transform: translateY(-2px);
1849
+ }
1850
+
1851
+ .weight-stats {
1852
+ display: grid;
1853
+ grid-template-columns: repeat(3, 1fr);
1854
+ gap: var(--spacing-md);
1855
+ margin-bottom: var(--spacing-lg);
1856
+ }
1857
+
1858
+ .weight-stat {
1859
+ text-align: center;
1860
+ padding: var(--spacing-md);
1861
+ background: var(--bg-light);
1862
+ border-radius: var(--radius-md);
1863
+ }
1864
+
1865
+ .weight-stat.success {
1866
+ background: linear-gradient(135deg, #E8F5E9 0%, #C8E6C9 100%);
1867
+ }
1868
+
1869
+ .weight-stat-label {
1870
+ font-size: 0.75rem;
1871
+ color: var(--text-secondary);
1872
+ margin-bottom: var(--spacing-xs);
1873
+ }
1874
+
1875
+ .weight-stat-value {
1876
+ font-size: 1.1rem;
1877
+ font-weight: 700;
1878
+ color: var(--text-primary);
1879
+ }
1880
+
1881
+ .weight-progress-bar {
1882
+ width: 100%;
1883
+ height: 8px;
1884
+ background: var(--border);
1885
+ border-radius: var(--radius-full);
1886
+ overflow: hidden;
1887
+ margin-bottom: var(--spacing-lg);
1888
+ }
1889
+
1890
+ .weight-progress-fill {
1891
+ height: 100%;
1892
+ background: var(--gradient-primary);
1893
+ transition: width 0.5s ease;
1894
+ }
1895
+
1896
+ .weight-chart-mini {
1897
+ height: 100px;
1898
+ display: flex;
1899
+ align-items: flex-end;
1900
+ gap: 4px;
1901
+ padding: var(--spacing-md) 0;
1902
+ }
1903
+
1904
+ .weight-chart-bar {
1905
+ flex: 1;
1906
+ background: var(--gradient-primary);
1907
+ border-radius: 4px 4px 0 0;
1908
+ min-height: 20px;
1909
+ transition: height 0.3s ease;
1910
+ }
1911
+
1912
+ /* Detailed Statistics */
1913
+ .detailed-stats-section {
1914
+ margin-bottom: var(--spacing-xl);
1915
+ }
1916
+
1917
+ .detailed-stats-section h3 {
1918
+ font-size: 1.25rem;
1919
+ font-weight: 600;
1920
+ margin-bottom: var(--spacing-md);
1921
+ }
1922
+
1923
+ .stats-grid {
1924
+ display: grid;
1925
+ grid-template-columns: repeat(2, 1fr);
1926
+ gap: var(--spacing-md);
1927
+ }
1928
+
1929
+ .stat-detail-card {
1930
+ background: var(--white);
1931
+ border-radius: var(--radius-lg);
1932
+ padding: var(--spacing-lg);
1933
+ box-shadow: var(--shadow-sm);
1934
+ display: flex;
1935
+ gap: var(--spacing-md);
1936
+ }
1937
+
1938
+ .stat-detail-icon {
1939
+ font-size: 36px;
1940
+ flex-shrink: 0;
1941
+ }
1942
+
1943
+ .stat-detail-content {
1944
+ flex: 1;
1945
+ }
1946
+
1947
+ .stat-detail-number {
1948
+ font-size: 1.75rem;
1949
+ font-weight: 700;
1950
+ background: var(--gradient-primary);
1951
+ -webkit-background-clip: text;
1952
+ -webkit-text-fill-color: transparent;
1953
+ background-clip: text;
1954
+ line-height: 1;
1955
+ margin-bottom: var(--spacing-xs);
1956
+ }
1957
+
1958
+ .stat-detail-label {
1959
+ font-size: 0.85rem;
1960
+ color: var(--text-primary);
1961
+ font-weight: 500;
1962
+ margin-bottom: 4px;
1963
+ }
1964
+
1965
+ .stat-detail-sublabel {
1966
+ font-size: 0.75rem;
1967
+ color: var(--text-secondary);
1968
+ }
1969
+
1970
+ /* Weekly Activity Chart */
1971
+ .activity-chart-section {
1972
+ margin-bottom: var(--spacing-xl);
1973
+ }
1974
+
1975
+ .activity-chart-section h3 {
1976
+ font-size: 1.25rem;
1977
+ font-weight: 600;
1978
+ margin-bottom: var(--spacing-md);
1979
+ }
1980
+
1981
+ .weekly-chart {
1982
+ background: var(--white);
1983
+ border-radius: var(--radius-lg);
1984
+ padding: var(--spacing-lg);
1985
+ box-shadow: var(--shadow-sm);
1986
+ }
1987
+
1988
+ .chart-bars {
1989
+ display: flex;
1990
+ align-items: flex-end;
1991
+ justify-content: space-around;
1992
+ gap: var(--spacing-sm);
1993
+ height: 150px;
1994
+ }
1995
+
1996
+ .chart-day {
1997
+ flex: 1;
1998
+ display: flex;
1999
+ flex-direction: column;
2000
+ align-items: center;
2001
+ gap: var(--spacing-xs);
2002
+ }
2003
+
2004
+ .chart-bar {
2005
+ width: 100%;
2006
+ background: var(--gradient-primary);
2007
+ border-radius: 4px 4px 0 0;
2008
+ min-height: 4px;
2009
+ transition: height 0.3s ease;
2010
+ }
2011
+
2012
+ .chart-label {
2013
+ font-size: 0.7rem;
2014
+ color: var(--text-secondary);
2015
+ font-weight: 500;
2016
+ }
2017
+
2018
+ /* Records Section */
2019
+ .records-section {
2020
+ margin-bottom: var(--spacing-xl);
2021
+ }
2022
+
2023
+ .records-section h3 {
2024
+ font-size: 1.25rem;
2025
+ font-weight: 600;
2026
+ margin-bottom: var(--spacing-md);
2027
+ }
2028
+
2029
+ .records-list {
2030
+ display: flex;
2031
+ flex-direction: column;
2032
+ gap: var(--spacing-sm);
2033
+ }
2034
+
2035
+ .record-item {
2036
+ background: var(--white);
2037
+ border-radius: var(--radius-md);
2038
+ padding: var(--spacing-md);
2039
+ box-shadow: var(--shadow-sm);
2040
+ display: flex;
2041
+ align-items: center;
2042
+ gap: var(--spacing-md);
2043
+ }
2044
+
2045
+ .record-icon {
2046
+ font-size: 28px;
2047
+ }
2048
+
2049
+ .record-content {
2050
+ flex: 1;
2051
+ }
2052
+
2053
+ .record-label {
2054
+ font-size: 0.85rem;
2055
+ color: var(--text-secondary);
2056
+ margin-bottom: 2px;
2057
+ }
2058
+
2059
+ .record-value {
2060
+ font-size: 1rem;
2061
+ font-weight: 600;
2062
+ color: var(--text-primary);
2063
+ }
2064
+
2065
+ /* Weight Modal */
2066
+ .weight-input-group {
2067
+ margin-bottom: var(--spacing-md);
2068
+ }
2069
+
2070
+ .weight-input-group label {
2071
+ display: block;
2072
+ font-size: 0.9rem;
2073
+ font-weight: 500;
2074
+ color: var(--text-primary);
2075
+ margin-bottom: var(--spacing-xs);
2076
+ }
2077
+
2078
+ .weight-input-group input {
2079
+ width: 100%;
2080
+ padding: 12px 16px;
2081
+ border: 2px solid var(--border);
2082
+ border-radius: var(--radius-md);
2083
+ font-size: 1rem;
2084
+ font-family: inherit;
2085
+ transition: all 0.3s ease;
2086
+ }
2087
+
2088
+ .weight-input-group input:focus {
2089
+ outline: none;
2090
+ border-color: var(--primary);
2091
+ box-shadow: 0 0 0 3px rgba(255, 107, 157, 0.1);
2092
+ }
2093
+
2094
+ .modal-actions {
2095
+ display: flex;
2096
+ gap: var(--spacing-md);
2097
+ margin-top: var(--spacing-lg);
2098
+ }
2099
+
2100
+ .btn-modal-secondary {
2101
+ flex: 1;
2102
+ background: var(--bg-light);
2103
+ color: var(--text-primary);
2104
+ border: 2px solid var(--border);
2105
+ padding: 12px 24px;
2106
+ border-radius: var(--radius-full);
2107
+ font-size: 1rem;
2108
+ font-weight: 600;
2109
+ cursor: pointer;
2110
+ transition: all 0.3s ease;
2111
+ }
2112
+
2113
+ .btn-modal-secondary:hover {
2114
+ background: var(--border);
2115
+ }
2116
+
2117
+ .btn-modal-primary {
2118
+ flex: 1;
2119
+ background: var(--gradient-primary);
2120
+ color: var(--white);
2121
+ border: none;
2122
+ padding: 12px 24px;
2123
+ border-radius: var(--radius-full);
2124
+ font-size: 1rem;
2125
+ font-weight: 600;
2126
+ cursor: pointer;
2127
+ transition: all 0.3s ease;
2128
+ box-shadow: 0 4px 12px rgba(255, 107, 157, 0.3);
2129
+ }
2130
+
2131
+ .btn-modal-primary:hover {
2132
+ transform: translateY(-2px);
2133
+ box-shadow: 0 6px 16px rgba(255, 107, 157, 0.4);
2134
+ }
2135
+
2136
+ .btn-modal-primary:active {
2137
+ transform: translateY(0);
2138
+ }
2139
+
2140
+ /* Settings FAB */
2141
+ .settings-fab {
2142
+ position: fixed;
2143
+ bottom: 100px;
2144
+ right: 20px;
2145
+ z-index: 99;
2146
+ max-width: 480px;
2147
+ margin: 0 auto;
2148
+ }
2149
+
2150
+ .fab-settings {
2151
+ width: 56px;
2152
+ height: 56px;
2153
+ border-radius: var(--radius-full);
2154
+ background: var(--gradient-primary);
2155
+ border: none;
2156
+ box-shadow: var(--shadow-lg);
2157
+ cursor: pointer;
2158
+ display: flex;
2159
+ align-items: center;
2160
+ justify-content: center;
2161
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
2162
+ }
2163
+
2164
+ .fab-settings:hover {
2165
+ transform: scale(1.1);
2166
+ box-shadow: var(--shadow-xl);
2167
+ }
2168
+
2169
+ .fab-settings:active {
2170
+ transform: scale(0.95);
2171
+ }
2172
+
2173
+ .fab-icon {
2174
+ font-size: 24px;
2175
+ }
2176
+
2177
+ /* Video Lazy Loading Styles */
2178
+ .video-loading {
2179
+ position: relative;
2180
+ }
2181
+
2182
+ .video-loader {
2183
+ position: absolute;
2184
+ top: 50%;
2185
+ left: 50%;
2186
+ transform: translate(-50%, -50%);
2187
+ z-index: 10;
2188
+ }
2189
+
2190
+ .spinner {
2191
+ width: 40px;
2192
+ height: 40px;
2193
+ border: 4px solid rgba(255, 255, 255, 0.3);
2194
+ border-top-color: var(--primary);
2195
+ border-radius: var(--radius-full);
2196
+ animation: spin 0.8s linear infinite;
2197
+ }
2198
+
2199
+ @keyframes spin {
2200
+ to { transform: rotate(360deg); }
2201
+ }
2202
+
2203
+ .video-error {
2204
+ opacity: 0.5;
2205
+ }
2206
+
2207
+ .video-error::after {
2208
+ content: '⚠️ Error loading video';
2209
+ position: absolute;
2210
+ top: 50%;
2211
+ left: 50%;
2212
+ transform: translate(-50%, -50%);
2213
+ color: var(--white);
2214
+ background: rgba(0, 0, 0, 0.7);
2215
+ padding: var(--spacing-sm) var(--spacing-md);
2216
+ border-radius: var(--radius-md);
2217
+ font-size: 0.85rem;
2218
+ z-index: 10;
2219
+ }
2220
+
2221
+ /* Animations */
2222
+ @keyframes fadeIn {
2223
+ from { opacity: 0; }
2224
+ to { opacity: 1; }
2225
+ }
2226
+
2227
+ @keyframes pulse {
2228
+ 0%, 100% { transform: scale(1); }
2229
+ 50% { transform: scale(1.05); }
2230
+ }
2231
+
2232
+ .pulse {
2233
+ animation: pulse 1s ease infinite;
2234
+ }
2235
+
2236
+ @keyframes slideDown {
2237
+ from {
2238
+ opacity: 0;
2239
+ transform: translate(-50%, -20px);
2240
+ }
2241
+ to {
2242
+ opacity: 1;
2243
+ transform: translate(-50%, 0);
2244
+ }
2245
+ }
2246
+
2247
+ @keyframes slideUp {
2248
+ from {
2249
+ opacity: 1;
2250
+ transform: translate(-50%, 0);
2251
+ }
2252
+ to {
2253
+ opacity: 0;
2254
+ transform: translate(-50%, -20px);
2255
+ }
2256
+ }
2257
+
2258
+ /* Responsive - Mobile First */
2259
+ @media (max-width: 768px) {
2260
+ /* Ajustar padding geral */
2261
+ .view {
2262
+ padding: var(--spacing-sm);
2263
+ }
2264
+
2265
+ .main-view {
2266
+ padding: var(--spacing-sm);
2267
+ }
2268
+
2269
+ /* Header */
2270
+ .top-bar {
2271
+ padding: var(--spacing-sm) var(--spacing-md);
2272
+ }
2273
+
2274
+ .user-text .greeting {
2275
+ font-size: 0.9rem;
2276
+ }
2277
+
2278
+ .user-text .streak {
2279
+ font-size: 0.75rem;
2280
+ }
2281
+
2282
+ /* Fix overflow issues */
2283
+ body {
2284
+ overflow-x: hidden;
2285
+ }
2286
+
2287
+ .app-container {
2288
+ overflow-x: hidden;
2289
+ max-width: 100vw;
2290
+ }
2291
+
2292
+ /* Grid de categorias e cards */
2293
+ .category-grid {
2294
+ grid-template-columns: repeat(2, 1fr);
2295
+ gap: var(--spacing-sm);
2296
+ }
2297
+
2298
+ .action-cards {
2299
+ grid-template-columns: repeat(2, 1fr);
2300
+ gap: var(--spacing-sm);
2301
+ }
2302
+
2303
+ .wellness-grid {
2304
+ grid-template-columns: repeat(2, 1fr);
2305
+ gap: var(--spacing-sm);
2306
+ }
2307
+
2308
+ /* Estatísticas */
2309
+ .stats-grid {
2310
+ grid-template-columns: 1fr;
2311
+ gap: var(--spacing-sm);
2312
+ }
2313
+
2314
+ .today-stats {
2315
+ flex-direction: column;
2316
+ gap: var(--spacing-sm);
2317
+ }
2318
+
2319
+ /* Cards */
2320
+ .category-card,
2321
+ .action-card {
2322
+ padding: var(--spacing-md);
2323
+ }
2324
+
2325
+ .category-image,
2326
+ .action-icon {
2327
+ font-size: 2rem;
2328
+ }
2329
+
2330
+ /* Tipografia */
2331
+ .page-title {
2332
+ font-size: 1.5rem;
2333
+ }
2334
+
2335
+ .section-title {
2336
+ font-size: 1.1rem;
2337
+ }
2338
+
2339
+ /* Modais */
2340
+ .modal-content {
2341
+ width: 95%;
2342
+ max-width: none;
2343
+ margin: var(--spacing-md);
2344
+ }
2345
+
2346
+ /* Peso */
2347
+ .weight-stats {
2348
+ grid-template-columns: 1fr;
2349
+ gap: var(--spacing-sm);
2350
+ }
2351
+
2352
+ /* Workout Session */
2353
+ .workout-header {
2354
+ padding: var(--spacing-md);
2355
+ }
2356
+
2357
+ .demo-area {
2358
+ padding: var(--spacing-lg);
2359
+ }
2360
+
2361
+ .demo-icon {
2362
+ font-size: 4rem;
2363
+ }
2364
+
2365
+ /* Botões */
2366
+ .btn-back,
2367
+ .btn-primary,
2368
+ .btn-secondary {
2369
+ padding: 12px 20px;
2370
+ font-size: 0.9rem;
2371
+ }
2372
+
2373
+ /* Bottom Nav */
2374
+ .bottom-nav {
2375
+ padding: var(--spacing-sm) 0;
2376
+ }
2377
+
2378
+ .nav-item {
2379
+ min-width: 60px;
2380
+ }
2381
+
2382
+ .nav-icon {
2383
+ font-size: 22px;
2384
+ }
2385
+
2386
+ .nav-label {
2387
+ font-size: 0.7rem;
2388
+ }
2389
+
2390
+ /* FAB */
2391
+ .settings-fab {
2392
+ bottom: 80px;
2393
+ right: 15px;
2394
+ }
2395
+
2396
+ .fab-settings {
2397
+ width: 50px;
2398
+ height: 50px;
2399
+ }
2400
+ }
2401
+
2402
+ /* Fix para telas entre 360px-420px (como S23 FE, Galaxy A, etc) */
2403
+ @media (max-width: 420px) {
2404
+ /* Garantir que nada saia da tela */
2405
+ * {
2406
+ max-width: 100%;
2407
+ overflow-wrap: break-word;
2408
+ }
2409
+
2410
+ /* Modal de perfil */
2411
+ .modal-content {
2412
+ width: 95%;
2413
+ max-width: 95%;
2414
+ padding: var(--spacing-md);
2415
+ max-height: 95vh;
2416
+ }
2417
+
2418
+ .plan-modal-content {
2419
+ width: 95%;
2420
+ max-width: 95%;
2421
+ padding: var(--spacing-md);
2422
+ }
2423
+
2424
+ .modal-title {
2425
+ font-size: 1.25rem;
2426
+ }
2427
+
2428
+ .modal-actions {
2429
+ flex-direction: column;
2430
+ gap: var(--spacing-sm);
2431
+ }
2432
+
2433
+ .btn-modal-secondary,
2434
+ .btn-modal-primary {
2435
+ width: 100%;
2436
+ padding: 12px 16px;
2437
+ font-size: 0.95rem;
2438
+ }
2439
+
2440
+ /* Form no modal */
2441
+ .profile-form .form-row {
2442
+ flex-direction: column;
2443
+ }
2444
+
2445
+ .profile-form .form-group {
2446
+ width: 100%;
2447
+ }
2448
+
2449
+ .photo-preview {
2450
+ width: 120px;
2451
+ height: 120px;
2452
+ }
2453
+
2454
+ /* Ajustar cards de ação */
2455
+ .action-cards {
2456
+ grid-template-columns: repeat(2, 1fr);
2457
+ gap: var(--spacing-xs);
2458
+ }
2459
+
2460
+ .action-card {
2461
+ padding: var(--spacing-sm);
2462
+ min-height: 100px;
2463
+ }
2464
+
2465
+ .action-icon {
2466
+ font-size: 32px;
2467
+ }
2468
+
2469
+ .action-card h4 {
2470
+ font-size: 0.85rem;
2471
+ }
2472
+
2473
+ .action-card p {
2474
+ font-size: 0.75rem;
2475
+ }
2476
+
2477
+ /* Plano personalizado */
2478
+ .plan-card {
2479
+ padding: var(--spacing-md);
2480
+ }
2481
+
2482
+ .plan-header {
2483
+ flex-direction: column;
2484
+ gap: var(--spacing-sm);
2485
+ align-items: stretch;
2486
+ }
2487
+
2488
+ .plan-header h3 {
2489
+ font-size: 1rem;
2490
+ text-align: center;
2491
+ }
2492
+
2493
+ .btn-view-plan {
2494
+ width: 100%;
2495
+ padding: 10px;
2496
+ }
2497
+
2498
+ .plan-quick-stats {
2499
+ grid-template-columns: 1fr;
2500
+ gap: var(--spacing-xs);
2501
+ }
2502
+
2503
+ .plan-stat {
2504
+ padding: var(--spacing-sm);
2505
+ }
2506
+
2507
+ /* Progress circular */
2508
+ .daily-progress {
2509
+ padding: var(--spacing-md);
2510
+ }
2511
+
2512
+ .progress-circle {
2513
+ width: 100px;
2514
+ height: 100px;
2515
+ }
2516
+
2517
+ .progress-value {
2518
+ font-size: 1.5rem;
2519
+ }
2520
+
2521
+ /* Stats de hoje */
2522
+ .today-stats {
2523
+ gap: var(--spacing-xs);
2524
+ }
2525
+
2526
+ .stat {
2527
+ padding: var(--spacing-sm);
2528
+ }
2529
+
2530
+ .stat-value {
2531
+ font-size: 1rem;
2532
+ }
2533
+
2534
+ /* Top bar */
2535
+ .top-bar {
2536
+ padding: var(--spacing-sm);
2537
+ }
2538
+
2539
+ .avatar {
2540
+ width: 40px;
2541
+ height: 40px;
2542
+ font-size: 20px;
2543
+ }
2544
+
2545
+ .greeting {
2546
+ font-size: 0.85rem;
2547
+ }
2548
+
2549
+ .streak {
2550
+ font-size: 0.7rem;
2551
+ }
2552
+
2553
+ /* Weekly Activity */
2554
+ .weekly-activity-grid {
2555
+ grid-template-columns: repeat(7, 1fr);
2556
+ gap: 4px;
2557
+ padding: var(--spacing-sm);
2558
+ }
2559
+
2560
+ .weekly-day {
2561
+ padding: 4px;
2562
+ }
2563
+
2564
+ .weekly-day-name {
2565
+ font-size: 0.6rem;
2566
+ }
2567
+
2568
+ .weekly-day-number {
2569
+ font-size: 0.9rem;
2570
+ }
2571
+
2572
+ .weekly-day-workouts {
2573
+ font-size: 0.55rem;
2574
+ }
2575
+
2576
+ /* Category grid */
2577
+ .category-grid {
2578
+ grid-template-columns: repeat(2, 1fr);
2579
+ gap: var(--spacing-xs);
2580
+ }
2581
+
2582
+ .category-card {
2583
+ padding: var(--spacing-sm);
2584
+ }
2585
+
2586
+ .category-image {
2587
+ font-size: 36px;
2588
+ }
2589
+
2590
+ /* Wellness grid */
2591
+ .wellness-grid {
2592
+ grid-template-columns: repeat(2, 1fr);
2593
+ gap: var(--spacing-xs);
2594
+ }
2595
+
2596
+ /* Bottom nav */
2597
+ .bottom-nav {
2598
+ padding: 6px 0;
2599
+ padding-bottom: calc(6px + env(safe-area-inset-bottom, 0px));
2600
+ }
2601
+
2602
+ .nav-item {
2603
+ padding: 4px;
2604
+ min-width: 50px;
2605
+ }
2606
+
2607
+ .nav-icon {
2608
+ font-size: 20px;
2609
+ }
2610
+
2611
+ .nav-label {
2612
+ font-size: 0.65rem;
2613
+ }
2614
+ }
2615
+
2616
+ @media (max-width: 480px) {
2617
+ /* Grids em coluna única para telas muito pequenas */
2618
+ .action-cards,
2619
+ .category-grid,
2620
+ .wellness-grid {
2621
+ grid-template-columns: 1fr;
2622
+ }
2623
+
2624
+ /* Progresso circular menor */
2625
+ .progress-circle {
2626
+ width: 100px;
2627
+ height: 100px;
2628
+ }
2629
+
2630
+ .progress-circle svg {
2631
+ width: 100px;
2632
+ height: 100px;
2633
+ }
2634
+
2635
+ .progress-value {
2636
+ font-size: 1.5rem;
2637
+ }
2638
+
2639
+ /* Exercícios */
2640
+ .exercise-card {
2641
+ padding: var(--spacing-sm);
2642
+ }
2643
+
2644
+ /* Water tracking */
2645
+ .water-glasses {
2646
+ gap: var(--spacing-xs);
2647
+ grid-template-columns: repeat(4, 1fr);
2648
+ }
2649
+
2650
+ .glass {
2651
+ font-size: 1.2rem;
2652
+ }
2653
+
2654
+ /* Modal adjustments */
2655
+ .modal-content {
2656
+ width: 90%;
2657
+ padding: var(--spacing-lg);
2658
+ margin: var(--spacing-md);
2659
+ max-height: 85vh;
2660
+ }
2661
+
2662
+ .plan-modal-content {
2663
+ width: 95%;
2664
+ max-width: none;
2665
+ max-height: 85vh;
2666
+ }
2667
+
2668
+ /* Workout session - video responsivo */
2669
+ .demo-placeholder {
2670
+ width: 95%;
2671
+ max-width: 100%;
2672
+ }
2673
+
2674
+ .demo-video {
2675
+ max-height: 50vh;
2676
+ }
2677
+
2678
+ .demo-icon {
2679
+ font-size: 60px;
2680
+ }
2681
+
2682
+ /* Bottom nav safe area */
2683
+ .bottom-nav {
2684
+ padding-bottom: env(safe-area-inset-bottom, var(--spacing-sm));
2685
+ }
2686
+ }
2687
+
2688
+ @media (max-width: 360px) {
2689
+ /* Ajustes para telas muito pequenas */
2690
+ .page-title {
2691
+ font-size: 1.3rem;
2692
+ }
2693
+
2694
+ .hero-section {
2695
+ padding: var(--spacing-md) 0;
2696
+ }
2697
+
2698
+ .stat-detail-number {
2699
+ font-size: 1.5rem;
2700
+ }
2701
+
2702
+ .weight-value {
2703
+ font-size: 2.5rem;
2704
+ }
2705
+
2706
+ /* Botões menores */
2707
+ .btn-primary,
2708
+ .btn-secondary {
2709
+ padding: 10px 16px;
2710
+ font-size: 0.85rem;
2711
+ }
2712
+ }
2713
+
2714
+ @media (min-width: 769px) {
2715
+ /* Otimizações para tablet/desktop */
2716
+ .app-container {
2717
+ max-width: 768px;
2718
+ margin: 0 auto;
2719
+ }
2720
+
2721
+ .category-grid {
2722
+ grid-template-columns: repeat(3, 1fr);
2723
+ }
2724
+
2725
+ .action-cards {
2726
+ grid-template-columns: repeat(3, 1fr);
2727
+ }
2728
+ }
2729
+
2730
+ /* SVG Gradient Definitions */
2731
+ svg defs {
2732
+ position: absolute;
2733
+ width: 0;
2734
+ height: 0;
2735
+ }
2736
+
public/sw-optimized.js ADDED
@@ -0,0 +1,392 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Optimized Service Worker
3
+ * Version: 4.0.0
4
+ *
5
+ * Features:
6
+ * - Stale-while-revalidate for HTML/CSS/JS
7
+ * - Cache-first for static assets
8
+ * - Network-first for API calls
9
+ * - Intelligent cache expiration
10
+ * - Background sync support
11
+ * - Push notifications support
12
+ */
13
+
14
+ const VERSION = '4.0.0';
15
+ const CACHE_PREFIX = 'fitness-pwa';
16
+ const CACHES = {
17
+ static: `${CACHE_PREFIX}-static-v${VERSION}`,
18
+ dynamic: `${CACHE_PREFIX}-dynamic-v${VERSION}`,
19
+ images: `${CACHE_PREFIX}-images-v${VERSION}`,
20
+ videos: `${CACHE_PREFIX}-videos-v${VERSION}`,
21
+ audio: `${CACHE_PREFIX}-audio-v${VERSION}`,
22
+ };
23
+
24
+ // Cache size limits (in number of items)
25
+ const CACHE_LIMITS = {
26
+ dynamic: 50,
27
+ images: 30,
28
+ videos: 5, // Videos are large, keep only 5
29
+ audio: 10,
30
+ };
31
+
32
+ // Static assets to cache immediately
33
+ const STATIC_ASSETS = [
34
+ '/',
35
+ '/index.html',
36
+ '/app.js',
37
+ '/styles.css',
38
+ '/lazy-video.js',
39
+ '/manifest.json',
40
+ '/icons/icon-192x192.svg',
41
+ '/icons/icon-512x512.svg',
42
+ ];
43
+
44
+ // =======================
45
+ // Installation
46
+ // =======================
47
+
48
+ self.addEventListener('install', (event) => {
49
+ console.log('[SW] Installing version', VERSION);
50
+
51
+ event.waitUntil(
52
+ caches.open(CACHES.static)
53
+ .then(cache => {
54
+ console.log('[SW] Caching static assets');
55
+ return cache.addAll(STATIC_ASSETS);
56
+ })
57
+ .then(() => self.skipWaiting())
58
+ .catch(err => console.error('[SW] Install failed:', err))
59
+ );
60
+ });
61
+
62
+ // =======================
63
+ // Activation
64
+ // =======================
65
+
66
+ self.addEventListener('activate', (event) => {
67
+ console.log('[SW] Activating version', VERSION);
68
+
69
+ event.waitUntil(
70
+ caches.keys()
71
+ .then(cacheNames => {
72
+ // Delete old caches
73
+ return Promise.all(
74
+ cacheNames
75
+ .filter(name => name.startsWith(CACHE_PREFIX) && !Object.values(CACHES).includes(name))
76
+ .map(name => {
77
+ console.log('[SW] Deleting old cache:', name);
78
+ return caches.delete(name);
79
+ })
80
+ );
81
+ })
82
+ .then(() => self.clients.claim())
83
+ .catch(err => console.error('[SW] Activation failed:', err))
84
+ );
85
+ });
86
+
87
+ // =======================
88
+ // Fetch Strategies
89
+ // =======================
90
+
91
+ self.addEventListener('fetch', (event) => {
92
+ const { request } = event;
93
+ const url = new URL(request.url);
94
+
95
+ // Skip cross-origin requests (except for known CDNs)
96
+ if (url.origin !== location.origin && !isTrustedOrigin(url.origin)) {
97
+ return;
98
+ }
99
+
100
+ // Route to appropriate strategy
101
+ if (isVideoRequest(request)) {
102
+ event.respondWith(handleVideoRequest(request));
103
+ } else if (isAudioRequest(request)) {
104
+ event.respondWith(handleAudioRequest(request));
105
+ } else if (isImageRequest(request)) {
106
+ event.respondWith(handleImageRequest(request));
107
+ } else if (isAPIRequest(request)) {
108
+ event.respondWith(handleAPIRequest(request));
109
+ } else if (isStaticAsset(request)) {
110
+ event.respondWith(handleStaticAsset(request));
111
+ } else {
112
+ event.respondWith(handleDynamicRequest(request));
113
+ }
114
+ });
115
+
116
+ // =======================
117
+ // Request Handlers
118
+ // =======================
119
+
120
+ // Stale-while-revalidate: Serve from cache, update in background
121
+ async function handleStaticAsset(request) {
122
+ const cache = await caches.open(CACHES.static);
123
+ const cachedResponse = await cache.match(request);
124
+
125
+ // Fetch in background and update cache
126
+ const fetchPromise = fetch(request)
127
+ .then(response => {
128
+ if (response && response.status === 200) {
129
+ cache.put(request, response.clone());
130
+ }
131
+ return response;
132
+ })
133
+ .catch(err => {
134
+ console.error('[SW] Fetch failed for static asset:', err);
135
+ return cachedResponse; // Return stale cache on error
136
+ });
137
+
138
+ // Return cached response immediately, or wait for network
139
+ return cachedResponse || fetchPromise;
140
+ }
141
+
142
+ // Cache-first with network fallback for videos
143
+ async function handleVideoRequest(request) {
144
+ const cache = await caches.open(CACHES.videos);
145
+ const cachedResponse = await cache.match(request);
146
+
147
+ if (cachedResponse) {
148
+ return cachedResponse;
149
+ }
150
+
151
+ try {
152
+ const response = await fetch(request);
153
+
154
+ if (response && response.status === 200) {
155
+ // Cache the video but enforce limits
156
+ await limitCacheSize(CACHES.videos, CACHE_LIMITS.videos);
157
+ cache.put(request, response.clone());
158
+ }
159
+
160
+ return response;
161
+ } catch (error) {
162
+ console.error('[SW] Video fetch failed:', error);
163
+ // Return a fallback or offline page
164
+ return new Response('Video unavailable offline', {
165
+ status: 503,
166
+ statusText: 'Service Unavailable'
167
+ });
168
+ }
169
+ }
170
+
171
+ // Cache-first with network fallback for audio
172
+ async function handleAudioRequest(request) {
173
+ const cache = await caches.open(CACHES.audio);
174
+ const cachedResponse = await cache.match(request);
175
+
176
+ if (cachedResponse) {
177
+ return cachedResponse;
178
+ }
179
+
180
+ try {
181
+ const response = await fetch(request);
182
+
183
+ if (response && response.status === 200) {
184
+ await limitCacheSize(CACHES.audio, CACHE_LIMITS.audio);
185
+ cache.put(request, response.clone());
186
+ }
187
+
188
+ return response;
189
+ } catch (error) {
190
+ console.error('[SW] Audio fetch failed:', error);
191
+ return cachedResponse; // Try cached even if old
192
+ }
193
+ }
194
+
195
+ // Cache-first with expiration for images
196
+ async function handleImageRequest(request) {
197
+ const cache = await caches.open(CACHES.images);
198
+ const cachedResponse = await cache.match(request);
199
+
200
+ if (cachedResponse) {
201
+ // Check if cache is still fresh (7 days)
202
+ const cacheDate = new Date(cachedResponse.headers.get('date'));
203
+ const now = new Date();
204
+ const age = (now - cacheDate) / (1000 * 60 * 60 * 24); // Days
205
+
206
+ if (age < 7) {
207
+ return cachedResponse;
208
+ }
209
+ }
210
+
211
+ try {
212
+ const response = await fetch(request);
213
+
214
+ if (response && response.status === 200) {
215
+ await limitCacheSize(CACHES.images, CACHE_LIMITS.images);
216
+ cache.put(request, response.clone());
217
+ }
218
+
219
+ return response;
220
+ } catch (error) {
221
+ return cachedResponse || createFallbackImage();
222
+ }
223
+ }
224
+
225
+ // Network-first for API requests
226
+ async function handleAPIRequest(request) {
227
+ try {
228
+ const response = await fetch(request);
229
+
230
+ // Cache successful GET requests
231
+ if (request.method === 'GET' && response && response.status === 200) {
232
+ const cache = await caches.open(CACHES.dynamic);
233
+ cache.put(request, response.clone());
234
+ }
235
+
236
+ return response;
237
+ } catch (error) {
238
+ // Fallback to cache for GET requests
239
+ if (request.method === 'GET') {
240
+ const cache = await caches.open(CACHES.dynamic);
241
+ const cachedResponse = await cache.match(request);
242
+
243
+ if (cachedResponse) {
244
+ return cachedResponse;
245
+ }
246
+ }
247
+
248
+ // Return error response
249
+ return new Response(JSON.stringify({ error: 'Offline' }), {
250
+ status: 503,
251
+ headers: { 'Content-Type': 'application/json' }
252
+ });
253
+ }
254
+ }
255
+
256
+ // Stale-while-revalidate for dynamic content
257
+ async function handleDynamicRequest(request) {
258
+ const cache = await caches.open(CACHES.dynamic);
259
+ const cachedResponse = await cache.match(request);
260
+
261
+ const fetchPromise = fetch(request)
262
+ .then(response => {
263
+ if (response && response.status === 200) {
264
+ cache.put(request, response.clone());
265
+ }
266
+ return response;
267
+ })
268
+ .catch(() => cachedResponse);
269
+
270
+ return cachedResponse || fetchPromise;
271
+ }
272
+
273
+ // =======================
274
+ // Utility Functions
275
+ // =======================
276
+
277
+ function isVideoRequest(request) {
278
+ return request.url.includes('/videos/') ||
279
+ request.url.match(/\.(mp4|webm|ogg)$/i);
280
+ }
281
+
282
+ function isAudioRequest(request) {
283
+ return request.url.includes('/songs/') ||
284
+ request.url.match(/\.(mp3|ogg|wav)$/i);
285
+ }
286
+
287
+ function isImageRequest(request) {
288
+ return request.url.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i);
289
+ }
290
+
291
+ function isAPIRequest(request) {
292
+ return request.url.includes('/api/');
293
+ }
294
+
295
+ function isStaticAsset(request) {
296
+ return request.url.match(/\.(js|css|woff2|woff|ttf|eot)$/i);
297
+ }
298
+
299
+ function isTrustedOrigin(origin) {
300
+ const trustedOrigins = [
301
+ 'https://fonts.googleapis.com',
302
+ 'https://fonts.gstatic.com',
303
+ ];
304
+ return trustedOrigins.includes(origin);
305
+ }
306
+
307
+ async function limitCacheSize(cacheName, maxItems) {
308
+ const cache = await caches.open(cacheName);
309
+ const keys = await cache.keys();
310
+
311
+ if (keys.length > maxItems) {
312
+ // Delete oldest entries (FIFO)
313
+ const deleteCount = keys.length - maxItems;
314
+ for (let i = 0; i < deleteCount; i++) {
315
+ await cache.delete(keys[i]);
316
+ }
317
+ }
318
+ }
319
+
320
+ function createFallbackImage() {
321
+ // Return a small SVG placeholder
322
+ const svg = `
323
+ <svg width="400" height="300" xmlns="http://www.w3.org/2000/svg">
324
+ <rect fill="#f0f0f0" width="400" height="300"/>
325
+ <text fill="#999" font-family="Arial" font-size="18" x="50%" y="50%" text-anchor="middle">
326
+ Image unavailable offline
327
+ </text>
328
+ </svg>
329
+ `;
330
+
331
+ return new Response(svg, {
332
+ headers: { 'Content-Type': 'image/svg+xml' }
333
+ });
334
+ }
335
+
336
+ // =======================
337
+ // Background Sync
338
+ // =======================
339
+
340
+ self.addEventListener('sync', (event) => {
341
+ console.log('[SW] Background sync triggered:', event.tag);
342
+
343
+ if (event.tag === 'sync-data') {
344
+ event.waitUntil(syncData());
345
+ }
346
+ });
347
+
348
+ async function syncData() {
349
+ try {
350
+ // Implement your background sync logic here
351
+ console.log('[SW] Syncing data...');
352
+
353
+ // Example: Send queued data to server
354
+ // const response = await fetch('/api/sync', { method: 'POST', body: ... });
355
+
356
+ return Promise.resolve();
357
+ } catch (error) {
358
+ console.error('[SW] Sync failed:', error);
359
+ return Promise.reject(error);
360
+ }
361
+ }
362
+
363
+ // =======================
364
+ // Push Notifications
365
+ // =======================
366
+
367
+ self.addEventListener('push', (event) => {
368
+ const data = event.data ? event.data.json() : {};
369
+
370
+ const options = {
371
+ body: data.body || 'New notification',
372
+ icon: '/icons/icon-192x192.svg',
373
+ badge: '/icons/icon-72x72.png',
374
+ vibrate: [200, 100, 200],
375
+ data: data,
376
+ };
377
+
378
+ event.waitUntil(
379
+ self.registration.showNotification(data.title || 'Fitness App', options)
380
+ );
381
+ });
382
+
383
+ self.addEventListener('notificationclick', (event) => {
384
+ event.notification.close();
385
+
386
+ event.waitUntil(
387
+ clients.openWindow(event.notification.data.url || '/')
388
+ );
389
+ });
390
+
391
+ console.log('[SW] Service Worker loaded, version', VERSION);
392
+
public/sw.js ADDED
@@ -0,0 +1,288 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const VERSION = '3.4.0'; // 🎥🎵🔔 HF integration + Notifications + PWA optimizations
2
+ const CACHE_NAME = `fitness-app-${VERSION}`;
3
+ const STATIC_CACHE = `static-${VERSION}`;
4
+ const DYNAMIC_CACHE = `dynamic-${VERSION}`;
5
+ const VIDEO_CACHE = `video-${VERSION}`;
6
+ const HF_VIDEO_CACHE = `hf-video-${VERSION}`; // 🎥 Cache para vídeos do Hugging Face
7
+ const HF_AUDIO_CACHE = `hf-audio-${VERSION}`; // 🎵 Cache para áudios do Hugging Face
8
+ const MAX_VIDEO_CACHE = 5; // Limita cache de vídeos locais
9
+ const MAX_HF_VIDEO_CACHE = 10; // Limita cache de vídeos do Hugging Face (CDN externo)
10
+ const MAX_HF_AUDIO_CACHE = 4; // Limita cache de áudios do HF (4 arquivos de áudio)
11
+
12
+ // 🔔 Performance: Cache de notificações
13
+ const NOTIFICATION_BADGE = 'notification-badge';
14
+
15
+ // Files to cache immediately
16
+ const STATIC_ASSETS = [
17
+ '/',
18
+ '/index.html',
19
+ '/app.js',
20
+ '/styles.css',
21
+ '/manifest.json',
22
+ '/icons/icon-192x192.svg',
23
+ '/icons/icon-512x512.svg'
24
+ ];
25
+
26
+ // Install event - cache static assets
27
+ self.addEventListener('install', (event) => {
28
+ event.waitUntil(
29
+ caches.open(STATIC_CACHE)
30
+ .then(cache => cache.addAll(STATIC_ASSETS))
31
+ .then(() => self.skipWaiting())
32
+ );
33
+ });
34
+
35
+ // Activate event - clean up old caches
36
+ self.addEventListener('activate', (event) => {
37
+ event.waitUntil(
38
+ caches.keys().then(keys => {
39
+ return Promise.all(
40
+ keys.filter(key =>
41
+ key !== STATIC_CACHE &&
42
+ key !== DYNAMIC_CACHE &&
43
+ key !== VIDEO_CACHE &&
44
+ key !== HF_VIDEO_CACHE &&
45
+ key !== HF_AUDIO_CACHE
46
+ ).map(key => caches.delete(key))
47
+ );
48
+ }).then(() => self.clients.claim())
49
+ );
50
+ });
51
+
52
+ // Fetch event - serve from cache, fallback to network
53
+ self.addEventListener('fetch', (event) => {
54
+ const { request } = event;
55
+ const url = new URL(request.url);
56
+
57
+ // 🎥 Hugging Face videos - cache first with size limit (optimized for external CDN)
58
+ if (url.hostname === 'huggingface.co' || url.hostname === 'cdn-lfs.huggingface.co') {
59
+ event.respondWith(
60
+ caches.open(HF_VIDEO_CACHE).then(cache => {
61
+ return cache.match(request).then(cachedResponse => {
62
+ if (cachedResponse) {
63
+ return cachedResponse;
64
+ }
65
+
66
+ return fetch(request, {
67
+ mode: 'cors',
68
+ credentials: 'omit'
69
+ }).then(networkResponse => {
70
+ // Cache only if successful
71
+ if (networkResponse && networkResponse.status === 200) {
72
+ cache.put(request, networkResponse.clone());
73
+
74
+ // Limit cache size (LRU)
75
+ cache.keys().then(keys => {
76
+ if (keys.length > MAX_HF_VIDEO_CACHE) {
77
+ cache.delete(keys[0]); // Delete oldest
78
+ }
79
+ });
80
+ }
81
+ return networkResponse;
82
+ });
83
+ });
84
+ }).catch(() => {
85
+ // Fallback: serve from cache even if outdated
86
+ return caches.match(request);
87
+ })
88
+ );
89
+ return;
90
+ }
91
+
92
+ // Skip other cross-origin requests
93
+ if (url.origin !== location.origin) {
94
+ return;
95
+ }
96
+
97
+ // Local videos - cache first with size limit (LRU strategy)
98
+ if (request.url.includes('/videos/')) {
99
+ event.respondWith(
100
+ caches.open(VIDEO_CACHE).then(cache => {
101
+ return cache.match(request).then(cachedResponse => {
102
+ if (cachedResponse) {
103
+ return cachedResponse;
104
+ }
105
+
106
+ return fetch(request).then(networkResponse => {
107
+ // Cache only if successful
108
+ if (networkResponse.status === 200) {
109
+ cache.put(request, networkResponse.clone());
110
+
111
+ // Limit cache size (LRU)
112
+ cache.keys().then(keys => {
113
+ if (keys.length > MAX_VIDEO_CACHE) {
114
+ cache.delete(keys[0]); // Delete oldest
115
+ }
116
+ });
117
+ }
118
+ return networkResponse;
119
+ });
120
+ });
121
+ }).catch(() => {
122
+ // Fallback: serve from cache even if outdated
123
+ return caches.match(request);
124
+ })
125
+ );
126
+ return;
127
+ }
128
+
129
+ // 🎵 Hugging Face audio - cache first (for workout sounds)
130
+ if ((url.hostname === 'huggingface.co' || url.hostname === 'cdn-lfs.huggingface.co') &&
131
+ (request.url.includes('.mp3') || request.url.includes('.ogg'))) {
132
+ event.respondWith(
133
+ caches.open(HF_AUDIO_CACHE).then(cache => {
134
+ return cache.match(request).then(cachedResponse => {
135
+ if (cachedResponse) {
136
+ return cachedResponse;
137
+ }
138
+
139
+ return fetch(request, {
140
+ mode: 'cors',
141
+ credentials: 'omit'
142
+ }).then(networkResponse => {
143
+ // Cache only if successful
144
+ if (networkResponse && networkResponse.status === 200) {
145
+ cache.put(request, networkResponse.clone());
146
+
147
+ // Limit cache size (LRU)
148
+ cache.keys().then(keys => {
149
+ if (keys.length > MAX_HF_AUDIO_CACHE) {
150
+ cache.delete(keys[0]); // Delete oldest
151
+ }
152
+ });
153
+ }
154
+ return networkResponse;
155
+ });
156
+ });
157
+ }).catch(() => {
158
+ // Fallback: serve from cache even if outdated
159
+ return caches.match(request);
160
+ })
161
+ );
162
+ return;
163
+ }
164
+
165
+ // Local audio - cache first
166
+ if (request.url.includes('/songs/')) {
167
+ event.respondWith(
168
+ caches.match(request)
169
+ .then(response => response || fetch(request)
170
+ .then(fetchResponse => {
171
+ return caches.open(DYNAMIC_CACHE).then(cache => {
172
+ cache.put(request, fetchResponse.clone());
173
+ return fetchResponse;
174
+ });
175
+ })
176
+ )
177
+ );
178
+ return;
179
+ }
180
+
181
+ // Static assets - cache first
182
+ event.respondWith(
183
+ caches.match(request)
184
+ .then(response => {
185
+ if (response) return response;
186
+
187
+ return fetch(request).then(fetchResponse => {
188
+ // Cache successful responses
189
+ if (fetchResponse.status === 200) {
190
+ const responseClone = fetchResponse.clone();
191
+ caches.open(DYNAMIC_CACHE).then(cache => {
192
+ cache.put(request, responseClone);
193
+ });
194
+ }
195
+ return fetchResponse;
196
+ });
197
+ })
198
+ .catch(() => {
199
+ // Offline fallback
200
+ if (request.destination === 'document') {
201
+ return caches.match('/index.html');
202
+ }
203
+ })
204
+ );
205
+ });
206
+
207
+ // 🔔 Notification click event
208
+ self.addEventListener('notificationclick', (event) => {
209
+ event.notification.close();
210
+
211
+ event.waitUntil(
212
+ clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
213
+ // Se já tem uma janela aberta, foca nela
214
+ for (const client of clientList) {
215
+ if (client.url.includes(self.registration.scope) && 'focus' in client) {
216
+ return client.focus();
217
+ }
218
+ }
219
+ // Senão, abre uma nova janela
220
+ if (clients.openWindow) {
221
+ return clients.openWindow('/');
222
+ }
223
+ })
224
+ );
225
+ });
226
+
227
+ // 🔔 Push notification event (para notificações push futuras)
228
+ self.addEventListener('push', (event) => {
229
+ if (!event.data) return;
230
+
231
+ try {
232
+ const data = event.data.json();
233
+ const options = {
234
+ body: data.body || 'Nova notificação do seu app fitness!',
235
+ icon: '/icons/icon-192x192.svg',
236
+ badge: '/icons/icon-72x72.png',
237
+ vibrate: [200, 100, 200],
238
+ data: data.data || {},
239
+ actions: [
240
+ { action: 'open', title: 'Abrir App', icon: '/icons/icon-96x96.svg' },
241
+ { action: 'close', title: 'Fechar', icon: '/icons/icon-96x96.svg' }
242
+ ]
243
+ };
244
+
245
+ event.waitUntil(
246
+ self.registration.showNotification(data.title || 'Fitness App', options)
247
+ );
248
+ } catch (error) {
249
+ console.error('Erro ao processar push notification:', error);
250
+ }
251
+ });
252
+
253
+ // ⚡ Background Sync (para sincronização offline)
254
+ self.addEventListener('sync', (event) => {
255
+ if (event.tag === 'sync-data') {
256
+ event.waitUntil(syncData());
257
+ }
258
+ });
259
+
260
+ async function syncData() {
261
+ // Placeholder para sincronização futura
262
+ console.log('Sincronizando dados...');
263
+ }
264
+
265
+ // 🔋 Performance: Periodic Background Sync (para PWAs avançados)
266
+ self.addEventListener('periodicsync', (event) => {
267
+ if (event.tag === 'daily-motivation') {
268
+ event.waitUntil(sendDailyMotivation());
269
+ }
270
+ });
271
+
272
+ async function sendDailyMotivation() {
273
+ const motivationalMessages = [
274
+ '💪 Hora de treinar! Seu corpo agradece!',
275
+ '✨ Você está mais perto do seu objetivo!',
276
+ '🔥 Continue assim! Cada dia conta!'
277
+ ];
278
+
279
+ const randomMessage = motivationalMessages[Math.floor(Math.random() * motivationalMessages.length)];
280
+
281
+ await self.registration.showNotification('Lembrete Diário', {
282
+ body: randomMessage,
283
+ icon: '/icons/icon-192x192.svg',
284
+ badge: '/icons/icon-72x72.png',
285
+ vibrate: [200, 100, 200]
286
+ });
287
+ }
288
+
scripts/analyze-bundle.js ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Bundle Analyzer
5
+ *
6
+ * Analyzes the size of all assets and provides recommendations
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ function getFileSize(filePath) {
13
+ try {
14
+ const stats = fs.statSync(filePath);
15
+ return stats.size;
16
+ } catch {
17
+ return 0;
18
+ }
19
+ }
20
+
21
+ function formatSize(bytes) {
22
+ return (bytes / 1024).toFixed(2) + ' KB';
23
+ }
24
+
25
+ function analyzeDirectory(dir, extensions = []) {
26
+ let files = [];
27
+
28
+ try {
29
+ const items = fs.readdirSync(dir);
30
+
31
+ for (const item of items) {
32
+ const fullPath = path.join(dir, item);
33
+ const stat = fs.statSync(fullPath);
34
+
35
+ if (stat.isDirectory()) {
36
+ files = files.concat(analyzeDirectory(fullPath, extensions));
37
+ } else if (stat.isFile()) {
38
+ if (extensions.length === 0 || extensions.some(ext => item.endsWith(ext))) {
39
+ files.push({
40
+ path: fullPath.replace(process.cwd(), ''),
41
+ size: stat.size,
42
+ name: item
43
+ });
44
+ }
45
+ }
46
+ }
47
+ } catch (error) {
48
+ console.error(`Error analyzing directory ${dir}:`, error.message);
49
+ }
50
+
51
+ return files;
52
+ }
53
+
54
+ console.log('📊 Bundle Analysis Report\n');
55
+ console.log('='.repeat(60));
56
+
57
+ const publicDir = path.join(__dirname, '..', 'public');
58
+
59
+ // Analyze Core Files
60
+ console.log('\n📦 Core Application Files:');
61
+ console.log('-'.repeat(60));
62
+
63
+ const coreFiles = [
64
+ { name: 'HTML', path: 'public/index.html' },
65
+ { name: 'JavaScript', path: 'public/app.js' },
66
+ { name: 'CSS', path: 'public/styles.css' },
67
+ { name: 'Service Worker', path: 'public/sw.js' },
68
+ { name: 'Manifest', path: 'public/manifest.json' }
69
+ ];
70
+
71
+ let totalCoreSize = 0;
72
+ for (const file of coreFiles) {
73
+ const size = getFileSize(file.path);
74
+ totalCoreSize += size;
75
+ console.log(` ${file.name.padEnd(15)} ${formatSize(size).padStart(12)}`);
76
+ }
77
+
78
+ console.log('-'.repeat(60));
79
+ console.log(` ${'TOTAL'.padEnd(15)} ${formatSize(totalCoreSize).padStart(12)}`);
80
+
81
+ // Analyze Audio Files
82
+ console.log('\n🎵 Audio Files:');
83
+ console.log('-'.repeat(60));
84
+
85
+ const audioFiles = analyzeDirectory(path.join(publicDir, 'songs'));
86
+ let totalAudioSize = 0;
87
+
88
+ if (audioFiles.length > 0) {
89
+ for (const file of audioFiles) {
90
+ totalAudioSize += file.size;
91
+ console.log(` ${file.name.padEnd(30)} ${formatSize(file.size).padStart(12)}`);
92
+ }
93
+ console.log('-'.repeat(60));
94
+ console.log(` ${'TOTAL'.padEnd(30)} ${formatSize(totalAudioSize).padStart(12)}`);
95
+ } else {
96
+ console.log(' No audio files found');
97
+ }
98
+
99
+ // Analyze Video Files
100
+ console.log('\n🎬 Video Files:');
101
+ console.log('-'.repeat(60));
102
+
103
+ const videoFiles = analyzeDirectory(path.join(publicDir, 'videos'));
104
+ let totalVideoSize = 0;
105
+
106
+ if (videoFiles.length > 0) {
107
+ for (const file of videoFiles) {
108
+ totalVideoSize += file.size;
109
+ console.log(` ${file.name.padEnd(30)} ${formatSize(file.size).padStart(12)}`);
110
+ }
111
+ console.log('-'.repeat(60));
112
+ console.log(` ${'TOTAL'.padEnd(30)} ${formatSize(totalVideoSize).padStart(12)}`);
113
+ } else {
114
+ console.log(' No video files found');
115
+ }
116
+
117
+ // Analyze Icons
118
+ console.log('\n🎨 Icon Files:');
119
+ console.log('-'.repeat(60));
120
+
121
+ const iconFiles = analyzeDirectory(path.join(publicDir, 'icons'));
122
+ let totalIconSize = 0;
123
+
124
+ if (iconFiles.length > 0) {
125
+ for (const file of iconFiles) {
126
+ totalIconSize += file.size;
127
+ console.log(` ${file.name.padEnd(25)} ${formatSize(file.size).padStart(12)}`);
128
+ }
129
+ console.log('-'.repeat(60));
130
+ console.log(` ${'TOTAL'.padEnd(25)} ${formatSize(totalIconSize).padStart(12)}`);
131
+ }
132
+
133
+ // Overall Summary
134
+ console.log('\n📊 Summary:');
135
+ console.log('='.repeat(60));
136
+
137
+ const totalSize = totalCoreSize + totalAudioSize + totalVideoSize + totalIconSize;
138
+
139
+ console.log(` Core Bundle: ${formatSize(totalCoreSize).padStart(12)}`);
140
+ console.log(` Audio Assets: ${formatSize(totalAudioSize).padStart(12)}`);
141
+ console.log(` Video Assets: ${formatSize(totalVideoSize).padStart(12)}`);
142
+ console.log(` Icons: ${formatSize(totalIconSize).padStart(12)}`);
143
+ console.log('-'.repeat(60));
144
+ console.log(` TOTAL: ${formatSize(totalSize).padStart(12)}`);
145
+
146
+ // Recommendations
147
+ console.log('\n💡 Recommendations:');
148
+ console.log('='.repeat(60));
149
+
150
+ if (totalCoreSize > 150000) {
151
+ console.log(' ⚠️ Core bundle is large (>150KB). Consider:');
152
+ console.log(' - Code splitting');
153
+ console.log(' - Minification (run: npm run minify)');
154
+ console.log(' - Tree shaking with a bundler (Vite/Rollup)');
155
+ }
156
+
157
+ if (totalAudioSize > 5000000) {
158
+ console.log(' ⚠️ Audio files are large (>5MB). Consider:');
159
+ console.log(' - Compressing to lower bitrate');
160
+ console.log(' - Using OGG format for better compression');
161
+ console.log(' - Implementing lazy loading');
162
+ }
163
+
164
+ if (totalVideoSize > 10000000) {
165
+ console.log(' ⚠️ Video files are large (>10MB). Consider:');
166
+ console.log(' - Compressing with H.265 codec');
167
+ console.log(' - Creating multiple quality versions');
168
+ console.log(' - Using video streaming service');
169
+ }
170
+
171
+ // Estimated Load Times
172
+ console.log('\n⏱️ Estimated Load Times (Core Bundle Only):');
173
+ console.log('='.repeat(60));
174
+
175
+ const speeds = [
176
+ { name: '5G', speed: 20000 }, // 20 Mbps
177
+ { name: '4G', speed: 5000 }, // 5 Mbps
178
+ { name: '3G', speed: 750 }, // 750 Kbps
179
+ { name: 'Slow 3G', speed: 400 } // 400 Kbps
180
+ ];
181
+
182
+ for (const { name, speed } of speeds) {
183
+ const timeSeconds = (totalCoreSize * 8) / (speed * 1000);
184
+ console.log(` ${name.padEnd(12)} ${timeSeconds.toFixed(2)} seconds`);
185
+ }
186
+
187
+ console.log('\n' + '='.repeat(60));
188
+ console.log('✅ Analysis complete!\n');
189
+
scripts/build-production.js ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Production Build Script with Advanced Optimization
5
+ *
6
+ * This script:
7
+ * 1. Minifies JavaScript with Terser (proper minification)
8
+ * 2. Minifies CSS with advanced optimization
9
+ * 3. Optimizes HTML
10
+ * 4. Generates gzip versions
11
+ * 5. Creates bundle report
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const zlib = require('zlib');
17
+
18
+ // Simple but effective minification without external dependencies
19
+ function minifyCSS(css) {
20
+ return css
21
+ // Remove comments
22
+ .replace(/\/\*[\s\S]*?\*\//g, '')
23
+ // Remove unnecessary whitespace
24
+ .replace(/\s+/g, ' ')
25
+ // Remove spaces around operators
26
+ .replace(/\s*([{}:;,>+~])\s*/g, '$1')
27
+ // Remove trailing semicolons
28
+ .replace(/;}/g, '}')
29
+ // Remove leading zeros
30
+ .replace(/([:\s])0+\.(\d+)/g, '$1.$2')
31
+ // Remove units from zero values
32
+ .replace(/([:\s])0(px|em|%|rem|vh|vw)/g, '$10')
33
+ // Shorten color codes
34
+ .replace(/#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3/gi, '#$1$2$3')
35
+ .trim();
36
+ }
37
+
38
+ function minifyJS(js) {
39
+ return js
40
+ // Remove single-line comments (but preserve URLs)
41
+ .replace(/([^:])\/\/.*/g, '$1')
42
+ // Remove multi-line comments
43
+ .replace(/\/\*[\s\S]*?\*\//g, '')
44
+ // Remove unnecessary whitespace while preserving strings
45
+ .replace(/\s+/g, ' ')
46
+ // Remove whitespace around operators (basic)
47
+ .replace(/\s*([{}()[\]:;,<>!=+\-*/%&|?])\s*/g, '$1')
48
+ // Remove whitespace around keywords
49
+ .replace(/\s*(return|const|let|var|if|else|for|while|function|class|new)\s+/g, ' $1 ')
50
+ .trim();
51
+ }
52
+
53
+ function minifyHTML(html) {
54
+ return html
55
+ // Remove comments
56
+ .replace(/<!--[\s\S]*?-->/g, '')
57
+ // Remove unnecessary whitespace
58
+ .replace(/\s+/g, ' ')
59
+ // Remove whitespace between tags
60
+ .replace(/>\s+</g, '><')
61
+ // Remove quotes from attributes where possible
62
+ .replace(/\s+(\w+)="([^"]*?)"/g, (match, attr, value) => {
63
+ if (/^[a-zA-Z0-9_-]+$/.test(value)) {
64
+ return ` ${attr}=${value}`;
65
+ }
66
+ return match;
67
+ })
68
+ .trim();
69
+ }
70
+
71
+ function gzipFile(content) {
72
+ return zlib.gzipSync(content, { level: 9 });
73
+ }
74
+
75
+ function formatSize(bytes) {
76
+ return (bytes / 1024).toFixed(2) + ' KB';
77
+ }
78
+
79
+ function getCompressionRatio(original, compressed) {
80
+ return ((1 - compressed / original) * 100).toFixed(1) + '%';
81
+ }
82
+
83
+ console.log('🚀 Production Build Starting...\n');
84
+ console.log('='.repeat(60));
85
+
86
+ const publicDir = path.join(__dirname, '..', 'public');
87
+ const distDir = path.join(__dirname, '..', 'dist');
88
+
89
+ // Create dist directory
90
+ if (!fs.existsSync(distDir)) {
91
+ fs.mkdirSync(distDir, { recursive: true });
92
+ }
93
+
94
+ const files = [
95
+ {
96
+ name: 'JavaScript',
97
+ input: path.join(publicDir, 'app.js'),
98
+ output: path.join(distDir, 'app.min.js'),
99
+ minify: minifyJS
100
+ },
101
+ {
102
+ name: 'CSS',
103
+ input: path.join(publicDir, 'styles.css'),
104
+ output: path.join(distDir, 'styles.min.css'),
105
+ minify: minifyCSS
106
+ },
107
+ {
108
+ name: 'HTML',
109
+ input: path.join(publicDir, 'index.html'),
110
+ output: path.join(distDir, 'index.html'),
111
+ minify: minifyHTML
112
+ },
113
+ {
114
+ name: 'Service Worker',
115
+ input: path.join(publicDir, 'sw.js'),
116
+ output: path.join(distDir, 'sw.min.js'),
117
+ minify: minifyJS
118
+ }
119
+ ];
120
+
121
+ let totalOriginalSize = 0;
122
+ let totalMinifiedSize = 0;
123
+ let totalGzipSize = 0;
124
+
125
+ const results = [];
126
+
127
+ for (const file of files) {
128
+ try {
129
+ console.log(`\n📦 Processing ${file.name}...`);
130
+ console.log('-'.repeat(60));
131
+
132
+ const content = fs.readFileSync(file.input, 'utf8');
133
+ const minified = file.minify(content);
134
+ const gzipped = gzipFile(minified);
135
+
136
+ // Write minified file
137
+ fs.writeFileSync(file.output, minified);
138
+
139
+ // Write gzipped file
140
+ fs.writeFileSync(file.output + '.gz', gzipped);
141
+
142
+ const originalSize = Buffer.byteLength(content, 'utf8');
143
+ const minifiedSize = Buffer.byteLength(minified, 'utf8');
144
+ const gzipSize = gzipped.length;
145
+
146
+ totalOriginalSize += originalSize;
147
+ totalMinifiedSize += minifiedSize;
148
+ totalGzipSize += gzipSize;
149
+
150
+ const result = {
151
+ name: file.name,
152
+ originalSize,
153
+ minifiedSize,
154
+ gzipSize,
155
+ minSavings: getCompressionRatio(originalSize, minifiedSize),
156
+ gzipSavings: getCompressionRatio(originalSize, gzipSize)
157
+ };
158
+
159
+ results.push(result);
160
+
161
+ console.log(` Original: ${formatSize(originalSize)}`);
162
+ console.log(` Minified: ${formatSize(minifiedSize)} (${result.minSavings} savings)`);
163
+ console.log(` Gzipped: ${formatSize(gzipSize)} (${result.gzipSavings} savings)`);
164
+ console.log(` ✅ Saved to: ${path.basename(file.output)}`);
165
+
166
+ } catch (error) {
167
+ console.error(` ❌ Error processing ${file.name}:`, error.message);
168
+ }
169
+ }
170
+
171
+ // Summary
172
+ console.log('\n\n📊 Build Summary');
173
+ console.log('='.repeat(60));
174
+ console.log(` Total Original Size: ${formatSize(totalOriginalSize)}`);
175
+ console.log(` Total Minified Size: ${formatSize(totalMinifiedSize)}`);
176
+ console.log(` Total Gzipped Size: ${formatSize(totalGzipSize)}`);
177
+ console.log('-'.repeat(60));
178
+ console.log(` Minification Savings: ${getCompressionRatio(totalOriginalSize, totalMinifiedSize)}`);
179
+ console.log(` Gzip Savings: ${getCompressionRatio(totalOriginalSize, totalGzipSize)}`);
180
+
181
+ // Performance recommendations
182
+ console.log('\n\n💡 Performance Insights');
183
+ console.log('='.repeat(60));
184
+
185
+ const estimatedLoadTime3G = (totalGzipSize / 1024 / 750) * 8; // 750 Kbps
186
+ const estimatedLoadTime4G = (totalGzipSize / 1024 / 5000) * 8; // 5 Mbps
187
+
188
+ console.log(` Estimated load time (3G): ${estimatedLoadTime3G.toFixed(2)}s`);
189
+ console.log(` Estimated load time (4G): ${estimatedLoadTime4G.toFixed(2)}s`);
190
+
191
+ if (totalGzipSize / 1024 > 100) {
192
+ console.log('\n ⚠️ Bundle still large. Consider:');
193
+ console.log(' • Code splitting');
194
+ console.log(' • Lazy loading non-critical features');
195
+ console.log(' • Tree shaking with modern bundler (Vite/Rollup)');
196
+ } else {
197
+ console.log('\n ✅ Bundle size is optimal!');
198
+ }
199
+
200
+ // Write build report
201
+ const report = {
202
+ buildDate: new Date().toISOString(),
203
+ files: results,
204
+ totals: {
205
+ originalSize: totalOriginalSize,
206
+ minifiedSize: totalMinifiedSize,
207
+ gzipSize: totalGzipSize,
208
+ minSavings: getCompressionRatio(totalOriginalSize, totalMinifiedSize),
209
+ gzipSavings: getCompressionRatio(totalOriginalSize, totalGzipSize)
210
+ },
211
+ performance: {
212
+ estimatedLoadTime3G: estimatedLoadTime3G.toFixed(2) + 's',
213
+ estimatedLoadTime4G: estimatedLoadTime4G.toFixed(2) + 's'
214
+ }
215
+ };
216
+
217
+ fs.writeFileSync(
218
+ path.join(distDir, 'build-report.json'),
219
+ JSON.stringify(report, null, 2)
220
+ );
221
+
222
+ console.log('\n\n✅ Production build complete!');
223
+ console.log(`📁 Output directory: ${distDir}`);
224
+ console.log(`📄 Build report: ${path.join(distDir, 'build-report.json')}\n`);
225
+
scripts/minify.js ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Simple Minification Script
5
+ *
6
+ * This script minifies CSS and JS files to reduce bundle size.
7
+ * For production use, consider using a proper build tool like Vite or Webpack.
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ // CSS Minification (simple)
14
+ function minifyCSS(css) {
15
+ return css
16
+ // Remove comments
17
+ .replace(/\/\*[\s\S]*?\*\//g, '')
18
+ // Remove whitespace
19
+ .replace(/\s+/g, ' ')
20
+ // Remove spaces around certain characters
21
+ .replace(/\s*{\s*/g, '{')
22
+ .replace(/\s*}\s*/g, '}')
23
+ .replace(/\s*:\s*/g, ':')
24
+ .replace(/\s*;\s*/g, ';')
25
+ .replace(/\s*,\s*/g, ',')
26
+ // Remove trailing semicolons
27
+ .replace(/;}/g, '}')
28
+ .trim();
29
+ }
30
+
31
+ // JavaScript Minification (basic - for production use Terser)
32
+ function minifyJS(js) {
33
+ return js
34
+ // Remove single-line comments (careful with URLs)
35
+ .replace(/\/\/[^\n]*/g, '')
36
+ // Remove multi-line comments
37
+ .replace(/\/\*[\s\S]*?\*\//g, '')
38
+ // Remove extra whitespace (basic)
39
+ .replace(/\s+/g, ' ')
40
+ .trim();
41
+ }
42
+
43
+ // Paths
44
+ const publicDir = path.join(__dirname, '..', 'public');
45
+ const cssFile = path.join(publicDir, 'styles.css');
46
+ const jsFile = path.join(publicDir, 'app.js');
47
+ const cssMinFile = path.join(publicDir, 'styles.min.css');
48
+ const jsMinFile = path.join(publicDir, 'app.min.js');
49
+
50
+ try {
51
+ // Minify CSS
52
+ console.log('🎨 Minifying CSS...');
53
+ const css = fs.readFileSync(cssFile, 'utf8');
54
+ const minifiedCSS = minifyCSS(css);
55
+ fs.writeFileSync(cssMinFile, minifiedCSS);
56
+
57
+ const cssOriginalSize = (css.length / 1024).toFixed(2);
58
+ const cssMinifiedSize = (minifiedCSS.length / 1024).toFixed(2);
59
+ const cssSavings = ((1 - minifiedCSS.length / css.length) * 100).toFixed(1);
60
+
61
+ console.log(` Original: ${cssOriginalSize} KB`);
62
+ console.log(` Minified: ${cssMinifiedSize} KB`);
63
+ console.log(` Savings: ${cssSavings}%`);
64
+
65
+ // Minify JS (basic)
66
+ console.log('\n⚡ Minifying JavaScript...');
67
+ const js = fs.readFileSync(jsFile, 'utf8');
68
+ const minifiedJS = minifyJS(js);
69
+ fs.writeFileSync(jsMinFile, minifiedJS);
70
+
71
+ const jsOriginalSize = (js.length / 1024).toFixed(2);
72
+ const jsMinifiedSize = (minifiedJS.length / 1024).toFixed(2);
73
+ const jsSavings = ((1 - minifiedJS.length / js.length) * 100).toFixed(1);
74
+
75
+ console.log(` Original: ${jsOriginalSize} KB`);
76
+ console.log(` Minified: ${jsMinifiedSize} KB`);
77
+ console.log(` Savings: ${jsSavings}%`);
78
+
79
+ // Total savings
80
+ const totalOriginal = css.length + js.length;
81
+ const totalMinified = minifiedCSS.length + minifiedJS.length;
82
+ const totalSavings = ((1 - totalMinified / totalOriginal) * 100).toFixed(1);
83
+
84
+ console.log(`\n📦 Total Bundle Size:`);
85
+ console.log(` Before: ${(totalOriginal / 1024).toFixed(2)} KB`);
86
+ console.log(` After: ${(totalMinified / 1024).toFixed(2)} KB`);
87
+ console.log(` Total Savings: ${totalSavings}%`);
88
+
89
+ console.log('\n✅ Minification complete!');
90
+ console.log('\n💡 For production, use Terser for JS and cssnano for CSS:');
91
+ console.log(' npm install -g terser cssnano-cli');
92
+ console.log(' terser public/app.js -o public/app.min.js -c -m');
93
+ console.log(' cssnano public/styles.css public/styles.min.css');
94
+
95
+ } catch (error) {
96
+ console.error('❌ Error during minification:', error.message);
97
+ process.exit(1);
98
+ }
99
+
scripts/optimize-images.js ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Image Optimization Script
5
+ *
6
+ * Provides recommendations for image optimization
7
+ * Note: Requires external tools for actual optimization
8
+ */
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+
13
+ function getFileSize(filePath) {
14
+ try {
15
+ const stats = fs.statSync(filePath);
16
+ return stats.size;
17
+ } catch {
18
+ return 0;
19
+ }
20
+ }
21
+
22
+ function formatSize(bytes) {
23
+ return (bytes / 1024).toFixed(2) + ' KB';
24
+ }
25
+
26
+ function analyzeImages(dir, extensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']) {
27
+ let images = [];
28
+
29
+ try {
30
+ const items = fs.readdirSync(dir);
31
+
32
+ for (const item of items) {
33
+ const fullPath = path.join(dir, item);
34
+ const stat = fs.statSync(fullPath);
35
+
36
+ if (stat.isDirectory()) {
37
+ images = images.concat(analyzeImages(fullPath, extensions));
38
+ } else if (stat.isFile()) {
39
+ const ext = path.extname(item).toLowerCase();
40
+ if (extensions.includes(ext)) {
41
+ images.push({
42
+ path: fullPath.replace(process.cwd(), ''),
43
+ name: item,
44
+ size: stat.size,
45
+ ext: ext
46
+ });
47
+ }
48
+ }
49
+ }
50
+ } catch (error) {
51
+ console.error(`Error analyzing directory ${dir}:`, error.message);
52
+ }
53
+
54
+ return images;
55
+ }
56
+
57
+ console.log('🖼️ Image Optimization Analysis\n');
58
+ console.log('='.repeat(60));
59
+
60
+ const publicDir = path.join(__dirname, '..', 'public');
61
+ const iconsDir = path.join(publicDir, 'icons');
62
+
63
+ // Analyze images
64
+ const images = analyzeImages(publicDir);
65
+
66
+ if (images.length === 0) {
67
+ console.log('\nNo images found to optimize.');
68
+ process.exit(0);
69
+ }
70
+
71
+ console.log(`\nFound ${images.length} images\n`);
72
+
73
+ // Group by type
74
+ const byType = {};
75
+ images.forEach(img => {
76
+ if (!byType[img.ext]) {
77
+ byType[img.ext] = [];
78
+ }
79
+ byType[img.ext].push(img);
80
+ });
81
+
82
+ // Report by type
83
+ let totalSize = 0;
84
+ let potentialSavings = 0;
85
+
86
+ for (const [ext, imgs] of Object.entries(byType)) {
87
+ console.log(`\n${ext.toUpperCase()} Images:`);
88
+ console.log('-'.repeat(60));
89
+
90
+ const typeSize = imgs.reduce((sum, img) => sum + img.size, 0);
91
+ totalSize += typeSize;
92
+
93
+ // Calculate potential savings based on type
94
+ let savings = 0;
95
+ let recommendation = '';
96
+
97
+ switch (ext) {
98
+ case '.png':
99
+ savings = typeSize * 0.4; // ~40% savings with compression
100
+ recommendation = 'Use pngquant or TinyPNG. Consider WebP format.';
101
+ break;
102
+ case '.jpg':
103
+ case '.jpeg':
104
+ savings = typeSize * 0.3; // ~30% savings
105
+ recommendation = 'Use mozjpeg or jpegoptim. Consider WebP format.';
106
+ break;
107
+ case '.svg':
108
+ savings = typeSize * 0.2; // ~20% savings
109
+ recommendation = 'Use SVGO to optimize.';
110
+ break;
111
+ case '.gif':
112
+ savings = typeSize * 0.5; // ~50% savings
113
+ recommendation = 'Convert to WebP or use gifsicle.';
114
+ break;
115
+ default:
116
+ savings = 0;
117
+ recommendation = 'Already optimized format.';
118
+ }
119
+
120
+ potentialSavings += savings;
121
+
122
+ // Show largest images
123
+ imgs.sort((a, b) => b.size - a.size);
124
+ imgs.slice(0, 5).forEach(img => {
125
+ console.log(` ${img.name.padEnd(30)} ${formatSize(img.size).padStart(12)}`);
126
+ });
127
+
128
+ if (imgs.length > 5) {
129
+ console.log(` ... and ${imgs.length - 5} more`);
130
+ }
131
+
132
+ console.log(` Total: ${formatSize(typeSize)}`);
133
+ console.log(` 💡 ${recommendation}`);
134
+ console.log(` Potential savings: ${formatSize(savings)}`);
135
+ }
136
+
137
+ console.log('\n\n📊 Summary');
138
+ console.log('='.repeat(60));
139
+ console.log(` Total images: ${images.length}`);
140
+ console.log(` Total size: ${formatSize(totalSize)}`);
141
+ console.log(` Potential savings: ${formatSize(potentialSavings)} (${((potentialSavings/totalSize)*100).toFixed(1)}%)`);
142
+
143
+ console.log('\n\n💡 Optimization Recommendations');
144
+ console.log('='.repeat(60));
145
+
146
+ console.log('\n1. Install optimization tools:');
147
+ console.log(' npm install -g svgo');
148
+ console.log(' # For PNG: brew install pngquant (macOS) or apt-get install pngquant (Linux)');
149
+ console.log(' # For JPEG: brew install mozjpeg (macOS) or apt-get install mozjpeg (Linux)');
150
+
151
+ console.log('\n2. Optimize SVG icons:');
152
+ console.log(' svgo -f public/icons -o public/icons-optimized');
153
+
154
+ console.log('\n3. Convert to WebP (best compression):');
155
+ console.log(' # For each image:');
156
+ console.log(' # cwebp input.png -q 80 -o output.webp');
157
+
158
+ console.log('\n4. Use modern image formats:');
159
+ console.log(' • WebP: Best compression, wide support');
160
+ console.log(' • AVIF: Better than WebP, limited support');
161
+ console.log(' • Provide fallbacks for older browsers');
162
+
163
+ console.log('\n5. Implement responsive images:');
164
+ console.log(' <picture>');
165
+ console.log(' <source srcset="image.webp" type="image/webp">');
166
+ console.log(' <source srcset="image.jpg" type="image/jpeg">');
167
+ console.log(' <img src="image.jpg" alt="...">');
168
+ console.log(' </picture>');
169
+
170
+ console.log('\n6. Use lazy loading:');
171
+ console.log(' <img src="..." loading="lazy" alt="...">');
172
+
173
+ console.log('\n\n🎯 Priority Actions:');
174
+ console.log('='.repeat(60));
175
+
176
+ // Find large images that need optimization
177
+ const largeImages = images.filter(img => img.size > 100 * 1024).sort((a, b) => b.size - a.size);
178
+
179
+ if (largeImages.length > 0) {
180
+ console.log('\n⚠️ Large images that need optimization (>100KB):');
181
+ largeImages.slice(0, 10).forEach((img, i) => {
182
+ console.log(` ${i + 1}. ${img.name} (${formatSize(img.size)})`);
183
+ });
184
+ } else {
185
+ console.log('\n✅ No large images found! All images are optimized.');
186
+ }
187
+
188
+ // Check for SVG optimization opportunities
189
+ const svgImages = images.filter(img => img.ext === '.svg');
190
+ if (svgImages.length > 0) {
191
+ console.log(`\n📝 ${svgImages.length} SVG files can be optimized with SVGO`);
192
+ }
193
+
194
+ console.log('\n' + '='.repeat(60));
195
+ console.log('✅ Analysis complete!\n');
196
+
server.js ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const cors = require('cors');
3
+ const helmet = require('helmet');
4
+ const compression = require('compression');
5
+ const path = require('path');
6
+
7
+ const app = express();
8
+ const PORT = process.env.PORT || 7860;
9
+
10
+ // Detect Hugging Face environment and configure accordingly
11
+ const isHuggingFace = process.env.SPACE_ID || process.env.SPACE_AUTHOR_NAME ||
12
+ process.env.HF_SPACE || process.env.GRADIO_SERVER_NAME ||
13
+ (process.env.HOST && process.env.HOST.includes('hf.space'));
14
+
15
+ console.log(`🌍 Environment: ${isHuggingFace ? 'Hugging Face' : 'Local/Other'}`);
16
+
17
+ // Secure CSP configuration
18
+ const cspDirectives = {
19
+ defaultSrc: ["'self'"],
20
+ styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
21
+ fontSrc: ["'self'", "https://fonts.gstatic.com", "data:"],
22
+ scriptSrc: ["'self'", "'unsafe-inline'"], // unsafe-inline needed for inline event handlers
23
+ imgSrc: ["'self'", "https:", "data:", "blob:"],
24
+ frameSrc: ["'self'", "https://www.youtube-nocookie.com", "https://www.youtube.com"],
25
+ connectSrc: ["'self'"],
26
+ mediaSrc: ["'self'", "https:", "data:"],
27
+ objectSrc: ["'none'"],
28
+ childSrc: ["'self'", "https://www.youtube-nocookie.com"],
29
+ workerSrc: ["'self'", "blob:"],
30
+ manifestSrc: ["'self'"],
31
+ baseUri: ["'self'"],
32
+ formAction: ["'self'"],
33
+ upgradeInsecureRequests: isHuggingFace ? [] : null,
34
+ };
35
+
36
+ if (isHuggingFace) {
37
+ console.log('🔒 Hugging Face detected - Using balanced CSP for compatibility and security');
38
+ // Add Hugging Face specific domains
39
+ cspDirectives.frameSrc.push("https://*.hf.space", "https://*.huggingface.co");
40
+ cspDirectives.connectSrc.push("https://*.hf.space", "https://*.huggingface.co");
41
+ } else {
42
+ console.log('🔒 Local environment - Using strict CSP');
43
+ }
44
+
45
+ app.use(helmet({
46
+ contentSecurityPolicy: {
47
+ directives: cspDirectives,
48
+ reportOnly: false // Set to true for testing, false for enforcement
49
+ },
50
+ crossOriginEmbedderPolicy: false, // Disabled for YouTube embeds
51
+ crossOriginResourcePolicy: { policy: "cross-origin" },
52
+ referrerPolicy: { policy: "strict-origin-when-cross-origin" },
53
+ strictTransportSecurity: {
54
+ maxAge: 31536000,
55
+ includeSubDomains: true,
56
+ preload: true
57
+ },
58
+ xContentTypeOptions: true,
59
+ xDnsPrefetchControl: { allow: false },
60
+ xDownloadOptions: true,
61
+ xFrameOptions: { action: isHuggingFace ? 'sameorigin' : 'deny' },
62
+ xPoweredBy: false,
63
+ xXssProtection: true
64
+ }));
65
+
66
+ // Configure CORS based on environment
67
+ // Whitelist of allowed origins
68
+ const allowedOrigins = [
69
+ 'http://localhost:7860',
70
+ 'http://localhost:3000',
71
+ 'https://*.hf.space',
72
+ 'https://*.huggingface.co'
73
+ ];
74
+
75
+ const corsOptions = {
76
+ origin: function (origin, callback) {
77
+ // Allow requests with no origin (mobile apps, curl, etc.)
78
+ if (!origin) return callback(null, true);
79
+
80
+ if (isHuggingFace) {
81
+ // For Hugging Face, allow all hf.space and huggingface.co origins
82
+ if (origin.includes('hf.space') || origin.includes('huggingface.co')) {
83
+ return callback(null, true);
84
+ }
85
+ }
86
+
87
+ // Allow local network IPs (192.168.x.x, 10.x.x.x, etc.)
88
+ if (origin.match(/^http:\/\/(localhost|127\.0\.0\.1|192\.168\.\d+\.\d+|10\.\d+\.\d+\.\d+)(:\d+)?$/)) {
89
+ return callback(null, true);
90
+ }
91
+
92
+ // Check if origin is in whitelist
93
+ const isAllowed = allowedOrigins.some(allowed => {
94
+ if (allowed.includes('*')) {
95
+ const pattern = allowed.replace(/\*/g, '.*');
96
+ return new RegExp(pattern).test(origin);
97
+ }
98
+ return allowed === origin;
99
+ });
100
+
101
+ if (isAllowed) {
102
+ callback(null, true);
103
+ } else {
104
+ console.log(`Blocked origin: ${origin}`);
105
+ callback(new Error('Not allowed by CORS'));
106
+ }
107
+ },
108
+ credentials: true,
109
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
110
+ allowedHeaders: ['Content-Type', 'Authorization'],
111
+ exposedHeaders: ['Content-Range', 'X-Content-Range'],
112
+ maxAge: 600 // Cache preflight requests for 10 minutes
113
+ };
114
+
115
+ app.use(cors(corsOptions));
116
+
117
+ // Additional security headers
118
+ app.use((req, res, next) => {
119
+ res.header('X-Content-Type-Options', 'nosniff');
120
+ res.header('X-Frame-Options', isHuggingFace ? 'SAMEORIGIN' : 'DENY');
121
+ res.header('Referrer-Policy', 'strict-origin-when-cross-origin');
122
+ next();
123
+ });
124
+
125
+ app.use(compression());
126
+ app.use(express.json());
127
+ app.use(express.static(path.join(__dirname, 'public')));
128
+
129
+ // Routes
130
+ app.get('/', (req, res) => {
131
+ res.sendFile(path.join(__dirname, 'public', 'index.html'));
132
+ });
133
+
134
+ app.get('/health', (req, res) => {
135
+ res.json({ status: 'OK', timestamp: new Date().toISOString() });
136
+ });
137
+
138
+ // API endpoints for progress tracking
139
+ app.post('/api/progress', (req, res) => {
140
+ // In a real app, this would save to a database
141
+ res.json({ success: true, message: 'Progress saved' });
142
+ });
143
+
144
+ app.get('/api/progress/:day', (req, res) => {
145
+ // In a real app, this would fetch from a database
146
+ res.json({ day: req.params.day, completed: [] });
147
+ });
148
+
149
+ const server = app.listen(PORT, '0.0.0.0', () => {
150
+ console.log(`🚀 30-Day Keto Planner running on port ${PORT}`);
151
+ console.log(`📱 Access at: http://localhost:${PORT}`);
152
+ console.log(`✅ Server status: RUNNING`);
153
+ });
154
+
155
+ // Graceful shutdown
156
+ process.on('SIGTERM', () => {
157
+ console.log('📴 SIGTERM received, shutting down gracefully');
158
+ server.close(() => {
159
+ console.log('💤 Process terminated');
160
+ process.exit(0);
161
+ });
162
+ });
163
+
164
+ process.on('SIGINT', () => {
165
+ console.log('📴 SIGINT received, shutting down gracefully');
166
+ server.close(() => {
167
+ console.log('💤 Process terminated');
168
+ process.exit(0);
169
+ });
170
+ });
171
+
172
+ // Handle server errors
173
+ server.on('error', (err) => {
174
+ if (err.code === 'EADDRINUSE') {
175
+ console.error(`❌ Port ${PORT} is already in use`);
176
+ console.log('💡 For local development, try a different port or stop other services');
177
+ console.log('🚀 For Hugging Face deployment, port 7860 will work correctly');
178
+ } else {
179
+ console.error('❌ Server error:', err);
180
+ }
181
+ process.exit(1);
182
+ });