Raí Santos commited on
Commit ·
20c5151
1
Parent(s): 73f05d7
oi
Browse files- .gitignore +108 -0
- Dockerfile +32 -0
- README.md +91 -4
- create-icons.js +35 -0
- dist/app.min.js +1 -0
- dist/app.min.js.gz +3 -0
- dist/build-report.json +48 -0
- dist/index.html +1 -0
- dist/index.html.gz +3 -0
- dist/styles.min.css +1 -0
- dist/styles.min.css.gz +3 -0
- dist/sw.min.js +1 -0
- dist/sw.min.js.gz +3 -0
- docker-compose.yml +17 -0
- generate-icons.html +61 -0
- package-lock.json +1303 -0
- package.json +38 -0
- public/app.js +0 -0
- public/icons/icon-128x128.svg +11 -0
- public/icons/icon-144x144.svg +11 -0
- public/icons/icon-152x152.svg +11 -0
- public/icons/icon-192x192.svg +11 -0
- public/icons/icon-384x384.svg +11 -0
- public/icons/icon-512x512.svg +11 -0
- public/icons/icon-72x72.png +0 -0
- public/icons/icon-72x72.svg +11 -0
- public/icons/icon-96x96.svg +11 -0
- public/icons/icon.svg +22 -0
- public/index.html +746 -0
- public/lazy-video.js +205 -0
- public/manifest.json +105 -0
- public/performance-monitor.js +422 -0
- public/styles.css +2736 -0
- public/sw-optimized.js +392 -0
- public/sw.js +288 -0
- scripts/analyze-bundle.js +189 -0
- scripts/build-production.js +225 -0
- scripts/minify.js +99 -0
- scripts/optimize-images.js +196 -0
- server.js +182 -0
.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:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: purple
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
---
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
});
|