Files
metabloom/App.tsx
T

57 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
type Tab = 'today' | 'journal' | 'progress' | 'profile';
type Entry = { date: string; weight: number; water: number; steps: number; glucose?: number };
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},
{date:'Mer',weight:91.1,water:1600,steps:6900,glucose:110},{date:'Jeu',weight:90.9,water:2000,steps:8200,glucose:105},
{date:'Ven',weight:90.8,water:2100,steps:8600,glucose:104},{date:'Sam',weight:90.7,water:1750,steps:7200,glucose:107},{date:'Auj.',weight:90.6,water:1250,steps:6240,glucose:103},
];
function Card({children, style}: any){return <View style={[s.card,style]}>{children}</View>}
function Pill({children, tone='green'}:any){return <View style={[s.pill,{backgroundColor:tone==='coral'?'#fff0eb':C.mint}]}><Text style={[s.pillText,{color:tone==='coral'?'#9b432a':C.green}]}>{children}</Text></View>}
function Metric({emoji,label,value,sub,onPress}:any){return <Pressable onPress={onPress} style={({pressed})=>[s.metric,pressed&&{opacity:.7}]}><Text style={s.metricEmoji}>{emoji}</Text><Text style={s.metricLabel}>{label}</Text><Text style={s.metricValue}>{value}</Text><Text style={s.metricSub}>{sub}</Text></Pressable>}
function ProgressBar({value,color=C.green}:any){return <View style={s.track}><View style={[s.fill,{width:`${Math.min(value,100)}%`,backgroundColor:color}]}/></View>}
function Nav({tab,setTab}:any){const items:[Tab,string,string][]=[['today','⌂','Aujourdhui'],['journal','','Journal'],['progress','↗','Progrès'],['profile','○','Profil']];return <View style={s.nav}>{items.map(([id,icon,label])=><Pressable accessibilityRole="button" key={id} onPress={()=>setTab(id)} style={s.navItem}><Text style={[s.navIcon,tab===id&&s.active]}>{icon}</Text><Text style={[s.navLabel,tab===id&&s.active]}>{label}</Text></Pressable>)}</View>}
export default function App(){
const [tab,setTab]=useState<Tab>('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 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'?<Today score={score} water={water} setWater={setWater} steps={steps} setSteps={setSteps} go={setTab}/>:tab==='journal'?<Journal weight={weight} setWeight={setWeight} glucose={glucose} setGlucose={setGlucose} water={water} setWater={setWater} steps={steps} setSteps={setSteps} save={save} saved={saved}/>:tab==='progress'?<Progress entries={entries}/>:<Profile reset={()=>{setEntries(seed);setWater(1250);setSteps(6240);AsyncStorage.removeItem('metabloom')}}/>,[tab,score,water,steps,weight,glucose,saved,entries]);
return <SafeAreaView style={s.safe}><StatusBar style="dark"/><View style={[s.shell,{maxWidth:width>900?1120:640}]}>{content}<Nav tab={tab} setTab={setTab}/></View></SafeAreaView>
}
function Header({eyebrow,title,right}:any){return <View style={s.header}><View><Text style={s.eyebrow}>{eyebrow}</Text><Text style={s.title}>{title}</Text></View>{right}</View>}
function Today({score,water,setWater,steps,setSteps,go}:any){return <ScrollView contentContainerStyle={s.page} showsVerticalScrollIndicator={false}>
<Header eyebrow="MARDI 21 JUILLET" title="Bonjour Vincent 👋" right={<View style={s.avatar}><Text style={s.avatarText}>V</Text></View>}/>
<LinearGradient colors={['#1d6b50','#2d8063']} style={s.hero}><View style={{flex:1}}><Pill>Aujourdhui</Pill><Text style={s.heroTitle}>Chaque petit pas compte.</Text><Text style={s.heroText}>Tu avances à ton rythme, sans pression. Encore une action douce pour compléter ta journée.</Text><Pressable onPress={()=>go('journal')} style={s.heroButton}><Text style={s.heroButtonText}>Ajouter une donnée </Text></Pressable></View><View style={s.ring}><Text style={s.ringBig}>{score}</Text><Text style={s.ringSmall}>/ 100</Text></View></LinearGradient>
<View style={s.sectionHead}><Text style={s.h2}>Mes essentiels</Text><Text style={s.caption}>Objectifs ajustables</Text></View>
<View style={s.grid}><Metric emoji="💧" label="Hydratation" value={`${water/1000} L`} sub="sur 2 L" onPress={()=>setWater((x:number)=>Math.min(x+250,2500))}/><Metric emoji="👟" label="Mouvement" value={steps.toLocaleString('fr-FR')} sub="sur 8 000 pas" onPress={()=>setSteps((x:number)=>x+500)}/><Metric emoji="🥗" label="Repas équilibrés" value="2 / 3" sub="très bien"/><Metric emoji="🌙" label="Sommeil" value="7 h 12" sub="objectif 8 h"/></View>
<Card style={s.tip}><View style={s.tipIcon}><Text style={{fontSize:22}}>☀️</Text></View><View style={{flex:1}}><Text style={s.tipTag}>PETIT CONSEIL DU JOUR</Text><Text style={s.tipTitle}>Une marche de 10 minutes après le repas ?</Text><Text style={s.body}>Un peu de mouvement aide à se sentir mieux. Adapte toujours leffort à ta situation.</Text></View></Card>
<Text style={s.disclaimer}>MetaBloom accompagne vos habitudes. Il ne remplace ni un diagnostic, ni un professionnel de santé.</Text>
</ScrollView>}
function Journal({weight,setWeight,glucose,setGlucose,water,setWater,steps,setSteps,save,saved}:any){return <ScrollView contentContainerStyle={s.page} keyboardShouldPersistTaps="handled"><Header eyebrow="SUIVI SIMPLE" title="Mon journal" right={<Pill>🔒 Local</Pill>}/><Text style={s.lead}>Note ce qui compte pour toi. Rien nest obligatoire.</Text>
<Card><Text style={s.h2}>Mesures du jour</Text><Text style={s.inputLabel}>Poids <Text style={s.optional}>facultatif</Text></Text><View style={s.inputRow}><TextInput accessibilityLabel="Poids en kilogrammes" value={weight} onChangeText={setWeight} keyboardType="decimal-pad" style={s.input}/><Text style={s.unit}>kg</Text></View><Text style={s.inputLabel}>Glycémie <Text style={s.optional}>facultatif · saisie manuelle</Text></Text><View style={s.inputRow}><TextInput accessibilityLabel="Glycémie" value={glucose} onChangeText={setGlucose} keyboardType="decimal-pad" style={s.input}/><Text style={s.unit}>mg/dL</Text></View><Text style={s.helper}>Ces valeurs servent uniquement à ton suivi personnel. Aucune interprétation médicale nest fournie.</Text></Card>
<Card><Text style={s.h2}>Habitudes</Text><Stepper icon="💧" label="Eau" value={`${water} ml`} minus={()=>setWater(Math.max(0,water-250))} plus={()=>setWater(water+250)}/><Stepper icon="👟" label="Pas" value={steps.toLocaleString('fr-FR')} minus={()=>setSteps(Math.max(0,steps-500))} plus={()=>setSteps(steps+500)}/></Card>
<Pressable onPress={save} style={({pressed})=>[s.primary,pressed&&{opacity:.8}]}><Text style={s.primaryText}>{saved?'✓ Journée enregistrée':'Enregistrer ma journée'}</Text></Pressable>
</ScrollView>}
function Stepper({icon,label,value,minus,plus}:any){return <View style={s.stepper}><Text style={{fontSize:24}}>{icon}</Text><View style={{flex:1}}><Text style={s.metricLabel}>{label}</Text><Text style={s.stepValue}>{value}</Text></View><Pressable onPress={minus} style={s.stepBtn}><Text style={s.stepBtnText}></Text></Pressable><Pressable onPress={plus} style={s.stepBtn}><Text style={s.stepBtnText}>+</Text></Pressable></View>}
function Progress({entries}:any){const min=Math.min(...entries.map((e:Entry)=>e.weight)); const max=Math.max(...entries.map((e:Entry)=>e.weight));return <ScrollView contentContainerStyle={s.page}><Header eyebrow="7 DERNIERS JOURS" title="Mes progrès" right={<Pill tone="coral"> 0,8 kg</Pill>}/><Text style={s.lead}>Observe les tendances, pas la perfection.</Text><Card><View style={s.sectionHead}><View><Text style={s.metricLabel}>Tendance de poids</Text><Text style={s.bigNumber}>{entries.at(-1).weight} <Text style={s.bigUnit}>kg</Text></Text></View><Pill>Progression douce</Pill></View><View style={s.chart}>{entries.map((e:Entry)=><View key={e.date} style={s.barCol}><Text style={s.barValue}>{e.weight}</Text><View style={[s.bar,{height:58+((max-e.weight)/(max-min||1))*55}]}/><Text style={s.barLabel}>{e.date}</Text></View>)}</View></Card>
<View style={s.grid}><Card style={s.stat}><Text style={s.statIcon}>💧</Text><Text style={s.bigNumber}>1,7 L</Text><Text style={s.body}>moyenne deau</Text></Card><Card style={s.stat}><Text style={s.statIcon}>👟</Text><Text style={s.bigNumber}>7 436</Text><Text style={s.body}>pas en moyenne</Text></Card></View>
<Card style={s.insight}><Text style={s.tipTag}>À RETENIR</Text><Text style={s.tipTitle}>Une semaine régulière 🌱</Text><Text style={s.body}>Tu as bougé au moins 6 000 pas chaque jour. Cette régularité compte davantage quune journée parfaite.</Text></Card></ScrollView>}
function Profile({reset}:any){return <ScrollView contentContainerStyle={s.page}><Header eyebrow="MON ESPACE" title="Profil & sécurité"/><Card style={s.profile}><View style={s.avatarLarge}><Text style={s.avatarLargeText}>V</Text></View><View><Text style={s.tipTitle}>Vincent</Text><Text style={s.body}>Objectif : habitudes durables</Text></View></Card><Text style={s.h2}>Préférences</Text>{[['🎯','Objectif principal','Équilibre métabolique'],['⚖️','Unité de poids','Kilogrammes (kg)'],['🩸','Unité glycémie','mg/dL'],['🔔','Rappels','À configurer']].map(x=><Card key={x[1]} style={s.setting}><Text style={{fontSize:22}}>{x[0]}</Text><View style={{flex:1}}><Text style={s.metricLabel}>{x[1]}</Text><Text style={s.body}>{x[2]}</Text></View><Text style={s.chevron}></Text></Card>)}<Card style={s.safety}><Text style={s.tipTag}>SÉCURITÉ & CONFIDENTIALITÉ</Text><Text style={s.tipTitle}>Vos données restent sur cet appareil.</Text><Text style={s.body}>Ce prototype nenvoie aucune donnée vers un serveur. Avant un usage réel, une validation clinique, RGPD/HDS et réglementaire sera nécessaire.</Text></Card><Pressable onPress={()=>Alert.alert('Effacer les données locales ?','Cette action réinitialise la démonstration.',[{text:'Annuler'},{text:'Effacer',style:'destructive',onPress:reset}])} style={s.danger}><Text style={s.dangerText}>Effacer mes données locales</Text></Pressable><Text style={s.disclaimer}>En cas de malaise ou durgence, contactez immédiatement les services durgence. MetaBloom ne fournit pas de conseil thérapeutique.</Text></ScrollView>}
const s=StyleSheet.create({safe:{flex:1,backgroundColor:C.cream},shell:{flex:1,width:'100%',alignSelf:'center'},page:{padding:20,paddingTop:26,paddingBottom:120,gap:16},header:{flexDirection:'row',justifyContent:'space-between',alignItems:'center',marginBottom:4},eyebrow:{fontSize:11,fontWeight:'800',letterSpacing:1.4,color:C.green},title:{fontSize:28,fontWeight:'800',color:C.ink,letterSpacing:-.7,marginTop:4},avatar:{width:44,height:44,borderRadius:22,backgroundColor:C.yellow,alignItems:'center',justifyContent:'center'},avatarText:{fontWeight:'800',color:C.ink,fontSize:17},hero:{borderRadius:28,padding:22,minHeight:230,flexDirection:'row',alignItems:'center',gap:14,overflow:'hidden'},heroTitle:{fontSize:27,lineHeight:32,fontWeight:'800',color:C.white,marginTop:18,maxWidth:360},heroText:{fontSize:14,lineHeight:21,color:'#dcebe4',marginTop:8,maxWidth:400},heroButton:{backgroundColor:C.white,borderRadius:14,paddingVertical:13,paddingHorizontal:16,alignSelf:'flex-start',marginTop:18},heroButtonText:{color:C.green,fontWeight:'800'},ring:{width:96,height:96,borderWidth:8,borderColor:C.yellow,borderRadius:50,alignItems:'center',justifyContent:'center',backgroundColor:'#ffffff13'},ringBig:{color:C.white,fontWeight:'900',fontSize:28},ringSmall:{color:'#dcebe4',fontSize:11},pill:{borderRadius:99,paddingHorizontal:11,paddingVertical:6,alignSelf:'flex-start'},pillText:{fontSize:11,fontWeight:'800'},sectionHead:{flexDirection:'row',justifyContent:'space-between',alignItems:'center'},h2:{fontSize:19,fontWeight:'800',color:C.ink},caption:{fontSize:12,color:C.muted},grid:{flexDirection:'row',flexWrap:'wrap',gap:12},metric:{backgroundColor:C.white,borderRadius:20,padding:17,width:'48%',minWidth:150,borderWidth:1,borderColor:C.line},metricEmoji:{fontSize:24,marginBottom:12},metricLabel:{fontSize:12,fontWeight:'700',color:C.muted},metricValue:{fontSize:22,fontWeight:'900',color:C.ink,marginTop:3},metricSub:{fontSize:11,color:C.muted,marginTop:3},card:{backgroundColor:C.white,borderRadius:22,padding:19,borderWidth:1,borderColor:C.line},tip:{flexDirection:'row',gap:14,backgroundColor:'#fffaf0'},tipIcon:{width:46,height:46,borderRadius:14,backgroundColor:C.yellow,alignItems:'center',justifyContent:'center'},tipTag:{fontSize:10,fontWeight:'900',letterSpacing:1.1,color:C.green,marginBottom:6},tipTitle:{fontSize:17,fontWeight:'800',color:C.ink,marginBottom:5},body:{fontSize:13,lineHeight:19,color:C.muted},disclaimer:{fontSize:11,lineHeight:17,textAlign:'center',color:C.muted,paddingHorizontal:16},nav:{position:'absolute',bottom:0,left:0,right:0,height:82,backgroundColor:'#fffffffa',borderTopWidth:1,borderColor:C.line,flexDirection:'row',paddingTop:9},navItem:{flex:1,alignItems:'center',justifyContent:'center',gap:3},navIcon:{fontSize:24,color:'#829087'},navLabel:{fontSize:10,fontWeight:'700',color:'#829087'},active:{color:C.green},lead:{fontSize:15,lineHeight:22,color:C.muted,marginTop:-8},inputLabel:{fontSize:13,fontWeight:'800',color:C.ink,marginTop:17,marginBottom:7},optional:{fontWeight:'500',color:C.muted},inputRow:{height:56,borderWidth:1,borderColor:C.line,borderRadius:14,flexDirection:'row',alignItems:'center',paddingHorizontal:15,backgroundColor:C.cream},input:{flex:1,fontSize:20,fontWeight:'800',color:C.ink,outlineStyle:'none'} as any,unit:{fontWeight:'700',color:C.muted},helper:{fontSize:11,lineHeight:16,color:C.muted,marginTop:12},stepper:{flexDirection:'row',alignItems:'center',gap:12,paddingVertical:13,borderBottomWidth:1,borderBottomColor:C.line},stepValue:{fontSize:17,fontWeight:'800',color:C.ink,marginTop:2},stepBtn:{width:42,height:42,borderRadius:13,backgroundColor:C.mint,alignItems:'center',justifyContent:'center'},stepBtnText:{fontSize:24,color:C.green,fontWeight:'700'},primary:{backgroundColor:C.green,borderRadius:16,padding:17,alignItems:'center'},primaryText:{color:C.white,fontSize:15,fontWeight:'900'},track:{height:8,borderRadius:4,backgroundColor:C.line,overflow:'hidden'},fill:{height:8,borderRadius:4},bigNumber:{fontSize:28,fontWeight:'900',color:C.ink,marginTop:4},bigUnit:{fontSize:14,color:C.muted},chart:{height:160,flexDirection:'row',alignItems:'flex-end',justifyContent:'space-between',marginTop:20},barCol:{flex:1,alignItems:'center'},bar:{width:22,backgroundColor:C.green,borderRadius:8,marginVertical:6},barValue:{fontSize:9,color:C.muted},barLabel:{fontSize:10,color:C.muted},stat:{width:'48%',minWidth:150},statIcon:{fontSize:22},insight:{backgroundColor:C.mint},profile:{flexDirection:'row',alignItems:'center',gap:15},avatarLarge:{width:62,height:62,borderRadius:22,backgroundColor:C.yellow,alignItems:'center',justifyContent:'center'},avatarLargeText:{fontSize:23,fontWeight:'900',color:C.ink},setting:{flexDirection:'row',alignItems:'center',gap:13,padding:15},chevron:{fontSize:28,color:C.muted},safety:{backgroundColor:C.mint},danger:{padding:16,alignItems:'center'},dangerText:{color:'#a23d33',fontWeight:'800'}});