diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..f4f0a8d --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,28 @@ +name: MetaBloom CI + +on: + push: + branches: [main] + pull_request: + +jobs: + app: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm run build:web + api: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install -r server/requirements-dev.txt + - run: pytest -q server/tests diff --git a/App.tsx b/App.tsx index 9270c97..ae06b50 100644 --- a/App.tsx +++ b/App.tsx @@ -2,10 +2,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage'; import { LinearGradient } from 'expo-linear-gradient'; import { StatusBar } from 'expo-status-bar'; import React, { useEffect, useMemo, useState } from 'react'; -import { Alert, Pressable, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, View, useWindowDimensions } from 'react-native'; +import { Alert, Pressable, SafeAreaView, ScrollView, Share, StyleSheet, Switch, Text, TextInput, View, useWindowDimensions } from 'react-native'; -type Tab = 'today' | 'journal' | 'progress' | 'profile'; -type Entry = { date: string; weight: number; water: number; steps: number; glucose?: number }; +type Tab = 'today' | 'journal' | 'progress' | 'coach' | 'profile'; +type Entry = { date: string; iso?: string; weight: number; water: number; steps: number; glucose?: number; sleep?: number; meals?: number; mood?: string }; +type ProfileData = { name:string; goal:string; journey:string; glucoseUnit:'mg/dL'|'mmol/L'; reminders:boolean; complete:boolean }; const C = { ink:'#15211c', muted:'#637169', cream:'#f6f7f2', white:'#fff', green:'#1d6b50', mint:'#dcefe5', coral:'#ef835f', yellow:'#f4d58d', line:'#e5e9e3' }; const seed: Entry[] = [ {date:'Lun',weight:91.4,water:1500,steps:6100,glucose:112},{date:'Mar',weight:91.2,water:1800,steps:7400,glucose:108}, @@ -17,22 +18,27 @@ function Card({children, style}: any){return {child function Pill({children, tone='green'}:any){return {children}} function Metric({emoji,label,value,sub,onPress}:any){return [s.metric,pressed&&{opacity:.7}]}>{emoji}{label}{value}{sub}} function ProgressBar({value,color=C.green}:any){return } -function Nav({tab,setTab}:any){const items:[Tab,string,string][]=[['today','⌂','Aujourd’hui'],['journal','+','Journal'],['progress','↗','Progrès'],['profile','○','Profil']];return {items.map(([id,icon,label])=>setTab(id)} style={s.navItem}>{icon}{label})}} +function Nav({tab,setTab}:any){const items:[Tab,string,string][]=[['today','⌂','Aujourd’hui'],['journal','+','Journal'],['progress','↗','Progrès'],['coach','✦','Coach'],['profile','○','Profil']];return {items.map(([id,icon,label])=>setTab(id)} style={s.navItem}>{icon}{label})}} export default function App(){ const [tab,setTab]=useState('today'); const [entries,setEntries]=useState(seed); const [water,setWater]=useState(1250); const [steps,setSteps]=useState(6240); - const [weight,setWeight]=useState('90.6'); const [glucose,setGlucose]=useState('103'); const [saved,setSaved]=useState(false); const {width}=useWindowDimensions(); - useEffect(()=>{AsyncStorage.getItem('metabloom').then(v=>{if(v){const d=JSON.parse(v);setEntries(d.entries||seed);setWater(d.water||1250);setSteps(d.steps||6240)}}).catch(()=>{})},[]); - useEffect(()=>{AsyncStorage.setItem('metabloom',JSON.stringify({entries,water,steps})).catch(()=>{})},[entries,water,steps]); + const [weight,setWeight]=useState('90.6'); const [glucose,setGlucose]=useState('103'); const [sleep,setSleep]=useState('7.2'); const [meals,setMeals]=useState(2); const [mood,setMood]=useState('🙂'); const [saved,setSaved]=useState(false); const [loaded,setLoaded]=useState(false); const [profile,setProfile]=useState({name:'',goal:'Retrouver mon équilibre',journey:'Prévention',glucoseUnit:'mg/dL',reminders:false,complete:false}); const {width}=useWindowDimensions(); + useEffect(()=>{AsyncStorage.getItem('metabloom-v2').then(v=>{if(v){const d=JSON.parse(v);setEntries(d.entries||seed);setWater(d.water||1250);setSteps(d.steps||6240);setProfile(d.profile||profile)}}).finally(()=>setLoaded(true))},[]); + useEffect(()=>{if(loaded)AsyncStorage.setItem('metabloom-v2',JSON.stringify({entries,water,steps,profile})).catch(()=>{})},[entries,water,steps,profile,loaded]); const score=Math.round((Math.min(water/2000,1)+Math.min(steps/8000,1)+.82+.75)*25); - const save=()=>{const w=parseFloat(weight.replace(',','.')); const g=parseFloat(glucose); if(!w||w<30||w>300){Alert.alert('Valeur à vérifier','Saisis un poids compris entre 30 et 300 kg.');return} setEntries(x=>[...x.slice(0,6),{date:'Auj.',weight:w,water,steps,glucose:g||undefined}]);setSaved(true);setTimeout(()=>setSaved(false),2200)}; - const content=useMemo(()=>tab==='today'?:tab==='journal'?:tab==='progress'?:{setEntries(seed);setWater(1250);setSteps(6240);AsyncStorage.removeItem('metabloom')}}/>,[tab,score,water,steps,weight,glucose,saved,entries]); - return 900?1120:640}]}>{content}