add new screens + home components + replace colors with theme tokens

New screens: Guides, Library, Map, Notifications, Settings.
Home refactored with modular components (SearchHeader, HomeCta, FrequentDiseases, SeasonAlert, PracticalGuides).
Replaced hardcoded colors with theme tokens in SeasonAlert and HomeCta.
Updated navigation, i18n, and CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yanis 2026-04-02 22:05:19 +02:00
parent 001658898e
commit af299e816a
22 changed files with 3089 additions and 442 deletions

View file

@ -1,7 +1,7 @@
# VinEye
Application mobile React Native (Expo) de détection de cépages par IA.
Analyse la vigne en temps réel via la caméra, identifie le cépage, et gamifie la progression.
Application mobile React Native (Expo) de detection de maladies de la vigne.
Cible des amateurs de vin/jardinage. Scan par camera, identification de maladies, bibliotheque de cepages, gamification.
---
@ -10,17 +10,16 @@ Analyse la vigne en temps réel via la caméra, identifie le cépage, et gamifie
| Couche | Technologies |
|--------|-------------|
| Framework | React Native + Expo SDK 54 (bare workflow) |
| Navigation | React Navigation v7 (NativeStack + BottomTabs) **PAS Expo Router** |
| Navigation | React Navigation v7 (NativeStack + BottomTabs) |
| Langage | TypeScript strict |
| UI | Composants custom (pas de shadcn — RN only) |
| Animations | React Native Reanimated v4 (`useEffect` vient de `react`, **pas** de reanimated) |
| IA | TFLite mock (weighted random : 70% vine / 20% uncertain / 10% not_vine) |
| Persistance | AsyncStorage (`@react-native-async-storage/async-storage`) |
| Styling | **NativeWind v4** (Tailwind) prioritaire, StyleSheet pour ombres/gradients |
| Icones | **lucide-react-native** (bottom bar) + **Ionicons** (reste de l'app) |
| Animations | React Native Reanimated v4 |
| IA | TFLite mock (weighted random) |
| Persistance | AsyncStorage |
| i18n | i18next + react-i18next (FR + EN) |
| Caméra | expo-camera |
| Camera | expo-camera |
| Haptics | expo-haptics |
| SVG | react-native-svg |
| Lottie | lottie-react-native |
| Package manager | **pnpm** |
---
@ -29,29 +28,25 @@ Analyse la vigne en temps réel via la caméra, identifie le cépage, et gamifie
```
VinEye/
├── App.tsx # Entry point (i18n init + RootNavigator)
├── App.tsx
├── src/
│ ├── assets/
│ │ ├── images/ # logo.svg, icon.png, splash.png
│ │ └── lottie/ # confetti.json, scan-success.json, vine-leaf.json, level-up.json
│ ├── components/
│ │ ├── ui/ # Button, Card, Badge, ProgressCircle, AnimatedCounter
│ │ ├── ui/ # Text, Button, Card, Badge, ProgressCircle
│ │ ├── home/ # SearchHeader, SearchSection, HomeCta, FrequentDiseases,
│ │ │ # SeasonAlert, PracticalGuides, statssection, gamificationstat
│ │ │ └── components/ # homeheader (SectionHeader)
│ │ ├── scanner/ # DetectionFrame, CameraOverlay, ConfidenceMeter
│ │ ├── gamification/ # XPBar, BadgeCard, LevelIndicator, StreakCounter
│ │ ├── gamification/ # XPBar, BadgeCard, ProgressRing, LevelIndicator
│ │ └── history/ # ScanCard, ScanList
│ ├── data/ # diseases.ts (7 maladies), guides.ts (3 guides)
│ ├── hooks/ # useDetection, useGameProgress, useHistory
│ ├── i18n/ # fr.json, en.json, index.ts
│ ├── navigation/ # RootNavigator, BottomTabNavigator, linking.ts
│ ├── screens/ # SplashScreen, HomeScreen, ScannerScreen, ResultScreen, HistoryScreen, ProfileScreen
│ ├── services/
│ │ ├── tflite/model.ts # Mock TFLite inference
│ │ ├── storage.ts # AsyncStorage wrapper typé
│ │ └── haptics.ts # hapticSuccess/Warning/Error/Light/Medium/Heavy
│ ├── theme/ # colors.ts, typography.ts, spacing.ts, index.ts
│ ├── screens/ # 11 ecrans (voir Navigation)
│ ├── services/ # tflite/model.ts, storage.ts, haptics.ts
│ ├── theme/ # colors.ts, typography.ts, spacing.ts
│ ├── types/ # detection.ts, gamification.ts, navigation.ts
│ └── utils/
│ ├── cepages.ts # 15 cépages (origine, couleur, caractéristiques, régions)
│ └── achievements.ts # XP_REWARDS, LEVELS, BADGE_DEFINITIONS, checkNewBadges, getLevelForXP
│ └── utils/ # cepages.ts, achievements.ts
```
---
@ -59,71 +54,137 @@ VinEye/
## Navigation
```
RootNavigator (Stack)
├── Splash → SplashScreen (auto-navigate vers Main après 2.8s)
RootNavigator (NativeStack)
├── Splash → SplashScreen (auto → Main apres 2.8s)
├── Main → BottomTabNavigator
│ ├── Home → HomeScreen
│ ├── Scanner → ScannerScreen (bouton FAB central)
│ ├── History → HistoryScreen
│ └── Profile → ProfileScreen
└── Result (modal) → ResultScreen (slide_from_bottom)
│ ├── Guides → GuidesScreen (tabs: Maladies / Guides Pratiques)
│ ├── Scanner → ScannerScreen (FAB central vert sureleve)
│ ├── Library → LibraryScreen (grille plantes scannees)
│ └── Map → MapScreen (placeholder)
├── Result (modal) → ResultScreen (slide_from_bottom)
├── Notifications → NotificationsScreen (slide_from_right)
├── Profile → ProfileScreen (slide_from_right)
├── Settings → SettingsScreen (slide_from_right)
├── Guides → GuidesScreen (aussi accessible via stack)
└── Library → LibraryScreen (aussi accessible via stack)
```
---
## Design Tokens (colors.ts)
| Token | Hex | Usage |
|-------|-----|-------|
| `primary[700]` | `#2D6A4F` | Tab active, CTA principal |
| `primary[800]` | `#1B4332` | Scanner FAB |
| `primary[900]` | `#0A2318` | Ombres |
| `accent[500]` | `#7C3AED` | Badges, accents violet raisin |
| `surface` | `#FFFFFF` | Fond tab bar, cards |
| `background` | `#F8FBF9` | Fond écrans |
| `neutral[300]` | `#D1D5DB` | Bordures |
| `neutral[400]` | `#9CA3AF` | Tab inactive |
**Bottom Tab Bar** : Home | Guides | Scanner (FAB) | Library | Map
- Icones : lucide-react-native (House, BookOpen, ScanLine, Leaf, Map)
- FAB Scanner : cercle vert primary[800], 56px, sureleve -28px
- Haptic feedback sur chaque onglet
---
## Gamification
## Ecrans
- **7 niveaux** : Bourgeon → Apprenti Vigneron → Vigneron → Expert Viticole → Sommelier → Grand Cru → Maître de Chai
- **XP** : +10 (vigne détectée), +5 (incertain), +15 (streak bonus)
- **7 badges** : premier_scan, amateur, expert, streaker_3, streaker_7, collectionneur, marathonien
- **Streak** : scan quotidien consécutif
| Ecran | Fichier | Description |
|-------|---------|-------------|
| Home | `screens/HomeScreen.tsx` | Header VinEye + search + CTA scan + maladies carousel + alerte saison + guides |
| Guides | `screens/GuidesScreen.tsx` | Segmented control (Maladies/Guides) + listes de cartes |
| Scanner | `screens/ScannerScreen.tsx` | Camera + detection IA |
| Library | `screens/LibraryScreen.tsx` | Grille 2 colonnes plantes scannees + favoris |
| Map | `screens/MapScreen.tsx` | Placeholder — a implementer |
| Result | `screens/ResultScreen.tsx` | Resultat scan + cepage + XP |
| Notifications | `screens/NotificationsScreen.tsx` | 3 types (alerte/conseil/systeme) + mock data |
| Profile | `screens/ProfileScreen.tsx` | Hero header vert + avatar + info card + stats Bento |
| Settings | `screens/SettingsScreen.tsx` | Menus groupes + referral card orange + reset |
| History | `screens/HistoryScreen.tsx` | Legacy — remplace par Notifications |
| Splash | `screens/SplashScreen.tsx` | Animation de demarrage |
---
## Fonctionnalités clés
## Composants Home
| Feature | Fichier principal | Statut |
|---------|-------------------|--------|
| Splash animée | `screens/SplashScreen.tsx` | ✅ |
| Scanner caméra | `screens/ScannerScreen.tsx` | ✅ |
| Résultat + cépage | `screens/ResultScreen.tsx` | ✅ |
| Historique + search | `screens/HistoryScreen.tsx` | ✅ |
| Profil + badges | `screens/ProfileScreen.tsx` | ✅ |
| Gamification XP | `hooks/useGameProgress.ts` | ✅ |
| Persistance | `services/storage.ts` | ✅ |
| Bilingue FR/EN | `i18n/` | ✅ |
| Composant | Fichier | Role |
|-----------|---------|------|
| SearchHeader | `components/home/SearchHeader.tsx` | Branding VinEye + greeting + boutons notifs/profil |
| SearchSection | `components/home/SearchSection.tsx` | Barre de recherche rounded-full avec filtre |
| HomeCta | `components/home/HomeCta.tsx` | Banner scan avec animation pulse + CTA |
| FrequentDiseases | `components/home/FrequentDiseases.tsx` | Carousel horizontal maladies (160px cards) |
| SeasonAlert | `components/home/SeasonAlert.tsx` | Carte alerte saisonniere (fond vert lime) |
| PracticalGuides | `components/home/PracticalGuides.tsx` | Liste verticale guides avec chevron |
| SectionHeader | `components/home/components/homeheader.tsx` | Titre section + bouton "Voir tout" |
---
## Donnees (Mock)
| Fichier | Contenu |
|---------|---------|
| `data/diseases.ts` | 7 maladies : mildiou, oidium, black rot, esca, botrytis, flavescence doree, chlorose |
| `data/guides.ts` | 3 guides : feuille saine, calendrier traitement, cepages bordelais |
---
## Design System
- **Fond** : `#F8F9FB` (gris bleuté)
- **Cards** : `#FFFFFF`, borderRadius 24-32, border 1px `#F0F0F0`
- **Ombres** : shadowOpacity 0.04, shadowRadius 8-10 (iOS), elevation 2-3 (Android)
- **Typographie** : Regular (400) par defaut, Medium (500) titres menus, Bold (700) noms utilisateur uniquement
- **Couleurs texte** : `#1A1A1A` (titres), `#8E8E93` (sous-titres/labels)
- **Style** : Bento Box minimaliste, espaces, zen
---
## Conventions
- **Styling** : NativeWind (className) prioritaire, StyleSheet pour ombres/gradients/arrondis specifiques
- Package manager : **pnpm**
- Path alias : `@/*``src/*`
- `useEffect` toujours depuis `react` (jamais depuis `react-native-reanimated`)
- Navigation : React Navigation v7 uniquement, **jamais Expo Router** (`src/app/` est interdit — renommé en `src/screens/`)
- `useEffect` depuis `react` (jamais depuis reanimated)
- Navigation : React Navigation v7, **jamais Expo Router**
- Max 300 lignes par fichier
- i18n : tous les textes via `t()`, cles dans fr.json et en.json
---
## Commandes
```bash
pnpm start # Lance Metro bundler
pnpm start # Metro bundler
pnpm web # Version web
pnpm android # Build Android
pnpm ios # Build iOS
```
---
## Changelog
### 2026-04-02 — Refonte navigation + nouveaux ecrans
#### Added
- Bottom tab bar classique avec FAB central (Home | Guides | Scanner FAB | Library | Map)
- Icones lucide-react-native pour la bottom bar
- SearchHeader : branding VinEye + greeting + boutons notifs/profil
- SearchSection : barre de recherche rounded-full avec filtre
- HomeCta : banner scan anime avec pulse reanimated
- FrequentDiseases : carousel horizontal 7 maladies (cards Bento 160px)
- SeasonAlert : carte alerte saisonniere
- PracticalGuides : liste verticale 3 guides
- NotificationsScreen : 3 types (alerte/conseil/systeme), 6 mock, mark all read, empty state
- ProfileScreen : hero header vert + avatar overlap + info card + stats Bento 2x2
- SettingsScreen : menus groupes + referral card orange + language toggle + reset
- GuidesScreen : segmented control (Maladies/Guides) + listes de cartes avec badges severite
- LibraryScreen : grille 2 colonnes plantes + toggle favoris coeur
- MapScreen : placeholder
- data/diseases.ts : 7 maladies de la vigne typees
- data/guides.ts : 3 guides pratiques types
- Traductions completes FR/EN pour tous les nouveaux ecrans
#### Changed
- Navigation restructuree : History/Profile retires du tab bar → accessibles via header
- HomeScreen simplifie : header + search + CTA + 3 sections contenu
- react-dom aligne sur react 19.1.0
#### Removed
- Ancien floating pill tab bar (LayoutAnimation buggue)
- StatisticsSection du HomeScreen (deplace vers ProfileScreen)
---
**Version** : 2.0.0
**Derniere mise a jour** : 2026-04-02

View file

@ -0,0 +1,149 @@
import { View, FlatList, TouchableOpacity, StyleSheet, Platform } from "react-native";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
import { VINE_DISEASES } from "@/data/diseases";
import type { Disease } from "@/data/diseases";
const DISEASE_TYPE_KEYS: Record<Disease["type"], string> = {
fungal: "diseases.types.fungal",
bacterial: "diseases.types.bacterial",
pest: "diseases.types.pest",
abiotic: "diseases.types.abiotic",
};
const SEVERITY_LEVELS: Record<Disease["severity"], { color: string; label: string }> = {
high: { color: "#EF4444", label: "high" },
medium: { color: "#F59E0B", label: "medium" },
low: { color: "#10B981", label: "low" },
};
export default function FrequentDiseases() {
const { t } = useTranslation();
return (
<FlatList
data={VINE_DISEASES}
keyExtractor={(item) => item.id}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.listContainer}
renderItem={({ item }) => {
const severity = SEVERITY_LEVELS[item.severity];
return (
<TouchableOpacity
activeOpacity={0.8}
style={[styles.card, { shadowColor: item.iconColor }]}
>
{/* Header: Icon & Severity Badge */}
<View style={styles.cardHeader}>
<View style={[styles.iconWrapper, { backgroundColor: `${item.iconColor}15` }]}>
<Ionicons name={item.icon as any} size={24} color={item.iconColor} />
</View>
<View style={[styles.severityBadge, { backgroundColor: `${severity.color}15` }]}>
<View style={[styles.dot, { backgroundColor: severity.color }]} />
</View>
</View>
{/* Content */}
<View style={styles.cardBody}>
<Text style={styles.typeText}>
{t(DISEASE_TYPE_KEYS[item.type]).toUpperCase()}
</Text>
<Text numberOfLines={2} style={styles.nameText}>
{t(item.name)}
</Text>
</View>
{/* Footer: Action hint */}
<View style={styles.cardFooter}>
<Text style={styles.moreInfo}>{t("common.details")}</Text>
<Ionicons name="chevron-forward" size={12} color={colors.neutral[400]} />
</View>
</TouchableOpacity>
);
}}
/>
);
}
const styles = StyleSheet.create({
listContainer: {
paddingHorizontal: 20,
paddingVertical: 10,
gap: 16,
},
card: {
width: 160,
backgroundColor: "#FFFFFF",
borderRadius: 24,
padding: 16,
justifyContent: "space-between",
// Shadow logic
...Platform.select({
ios: {
shadowOffset: { width: 0, height: 8 },
shadowOpacity: 0.12,
shadowRadius: 12,
},
android: {
elevation: 6,
},
}),
},
cardHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
marginBottom: 12,
},
iconWrapper: {
width: 44,
height: 44,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
},
severityBadge: {
padding: 6,
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
cardBody: {
flex: 1,
},
typeText: {
fontSize: 10,
fontWeight: "800",
color: colors.neutral[400],
letterSpacing: 0.5,
marginBottom: 4,
},
nameText: {
fontSize: 15,
fontWeight: "700",
color: colors.neutral[900],
lineHeight: 20,
},
cardFooter: {
flexDirection: "row",
alignItems: "center",
marginTop: 12,
gap: 4,
},
moreInfo: {
fontSize: 12,
fontWeight: "600",
color: colors.neutral[400],
}
});

View file

@ -0,0 +1,191 @@
import { useEffect } from "react";
import { View, TouchableOpacity, StyleSheet, Platform } from "react-native";
import { useTranslation } from "react-i18next";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { Ionicons, MaterialIcons } from "@expo/vector-icons";
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withSequence,
withTiming,
} from "react-native-reanimated";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
import type { RootStackParamList } from "@/types/navigation";
type Nav = NativeStackNavigationProp<RootStackParamList>;
export default function HeroScanner() {
const { t } = useTranslation();
const navigation = useNavigation<Nav>();
const pulse = useSharedValue(1);
useEffect(() => {
pulse.value = withRepeat(
withSequence(
withTiming(1.06, { duration: 1200 }),
withTiming(1, { duration: 1200 }),
),
-1,
false,
);
}, []);
const pulseStyle = useAnimatedStyle(() => ({
transform: [{ scale: pulse.value }],
}));
return (
<View style={styles.bannerContainer}>
{/* Decorative background */}
<View style={styles.gridOverlay} />
<View style={styles.leafDecorator}>
<Ionicons name="leaf" size={140} color={colors.primary[300]} />
</View>
{/* Header */}
<View style={styles.header}>
<Text style={styles.title}>{t("home.bannerTitle")}</Text>
<Text style={styles.subtitle}>{t("home.bannerSubtitle")}</Text>
</View>
{/* Animated scan zone */}
<View style={styles.scanZone}>
<Animated.View style={[styles.pulseOuter, pulseStyle]}>
<View style={styles.pulseInner}>
<View style={styles.iconCircle}>
<Ionicons name="scan-outline" size={38} color={colors.surface} />
</View>
</View>
</Animated.View>
</View>
{/* Main CTA button */}
<TouchableOpacity
activeOpacity={0.9}
onPress={() => navigation.navigate("Scanner")}
style={styles.mainButton}
>
<Text style={styles.buttonText}>{t("home.scanButton")}</Text>
<View style={styles.buttonIconWrapper}>
<MaterialIcons
name="arrow-forward"
size={18}
color={colors.primary[800]}
/>
</View>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
bannerContainer: {
marginHorizontal: 20,
marginBottom: 24,
borderRadius: 32,
padding: 24,
backgroundColor: colors.primary[600],
overflow: "hidden",
position: "relative",
...Platform.select({
ios: {
shadowColor: colors.primary[900],
shadowOffset: { width: 0, height: 12 },
shadowOpacity: 0.2,
shadowRadius: 16,
},
android: { elevation: 8 },
}),
},
leafDecorator: {
position: "absolute",
top: -20,
right: -20,
opacity: 0.15,
transform: [{ rotate: "-15deg" }],
},
gridOverlay: {
...StyleSheet.absoluteFillObject,
opacity: 0.05,
},
header: {
marginBottom: 20,
},
title: {
fontSize: 22,
fontWeight: "900",
color: colors.surface,
letterSpacing: -0.5,
},
subtitle: {
fontSize: 14,
color: colors.primary[200],
marginTop: 4,
maxWidth: "80%",
lineHeight: 20,
},
scanZone: {
alignItems: "center",
justifyContent: "center",
marginVertical: 20,
},
pulseOuter: {
width: 110,
height: 110,
borderRadius: 55,
backgroundColor: colors.primary[500] + "26",
alignItems: "center",
justifyContent: "center",
},
pulseInner: {
width: 85,
height: 85,
borderRadius: 42,
backgroundColor: colors.primary[400] + "40",
alignItems: "center",
justifyContent: "center",
borderWidth: 1,
borderColor: colors.primary[300] + "4D",
},
iconCircle: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: colors.primary[400] + "30",
alignItems: "center",
justifyContent: "center",
},
hintText: {
marginTop: 12,
fontSize: 11,
fontWeight: "700",
color: colors.primary[200],
textTransform: "uppercase",
letterSpacing: 1,
},
mainButton: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
backgroundColor: colors.surface,
borderRadius: 20,
paddingVertical: 14,
marginTop: 10,
},
buttonText: {
fontSize: 16,
fontWeight: "800",
color: colors.primary[900],
marginRight: 8,
},
buttonIconWrapper: {
backgroundColor: colors.primary[100],
padding: 4,
borderRadius: 8,
},
});

View file

@ -0,0 +1,112 @@
import { View, TouchableOpacity, StyleSheet, Platform } from "react-native";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
import { PRACTICAL_GUIDES } from "@/data/guides";
export default function PracticalGuides() {
const { t } = useTranslation();
return (
<View style={styles.container}>
{PRACTICAL_GUIDES.map((guide) => (
<TouchableOpacity
key={guide.id}
activeOpacity={0.6}
style={styles.card}
>
{/* Icône avec fond translucide assorti */}
<View
style={[
styles.iconContainer,
{ backgroundColor: `${guide.iconColor}12` } // 12 = ~7% d'opacité
]}
>
<Ionicons
name={guide.icon as any}
size={24}
color={guide.iconColor}
/>
</View>
{/* Textes */}
<View style={styles.textStack}>
<Text numberOfLines={1} style={styles.title}>
{t(guide.title)}
</Text>
<Text numberOfLines={1} style={styles.subtitle}>
{t(guide.subtitle)}
</Text>
</View>
{/* Indicateur d'action discret */}
<View style={styles.chevronWrapper}>
<Ionicons
name="chevron-forward"
size={16}
color={colors.neutral[300]}
/>
</View>
</TouchableOpacity>
))}
</View>
);
}
const styles = StyleSheet.create({
container: {
gap: 12,
paddingHorizontal: 4, // Pour ne pas couper l'ombre
},
card: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#FFFFFF",
borderRadius: 24, // Arrondi plus prononcé style "Bento"
padding: 14,
borderWidth: 1,
borderColor: "#F1F1F1",
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.04,
shadowRadius: 8,
},
android: {
elevation: 2,
},
}),
},
iconContainer: {
width: 52,
height: 52,
borderRadius: 18,
alignItems: "center",
justifyContent: "center",
},
textStack: {
flex: 1,
marginLeft: 14,
},
title: {
fontSize: 15,
fontWeight: "700",
color: colors.neutral[900],
letterSpacing: -0.3,
marginBottom: 2,
},
subtitle: {
fontSize: 13,
fontWeight: "500",
color: colors.neutral[500],
},
chevronWrapper: {
marginLeft: 8,
backgroundColor: "#F8F9FA",
padding: 6,
borderRadius: 12,
},
});

View file

@ -1,9 +1,10 @@
import { View, TouchableOpacity, TextInput } from "react-native";
import { View, TouchableOpacity, StyleSheet, Platform } from "react-native";
import { useTranslation } from "react-i18next";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { Ionicons } from "@expo/vector-icons";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
import type { RootStackParamList } from "@/types/navigation";
@ -14,44 +15,103 @@ export default function SearchHeader() {
const navigation = useNavigation<Nav>();
return (
<View className="flex-row items-center justify-between px-5 pt-3 pb-4">
<View className="flex-1 flex-row items-center gap-2">
<View className="flex-1 flex-row items-center rounded-full bg-neutral-200 px-3 py-2">
<Ionicons
name="search-outline"
size={18}
color={colors.neutral[500]}
/>
<TextInput
className="ml-2 flex-1 text-[14px]"
placeholder={t("history.search")}
placeholderTextColor={colors.neutral[500]}
style={{ color: colors.neutral[900], paddingVertical: 0 }}
/>
</View>
<View style={styles.headerContainer}>
<View style={styles.textContainer}>
<Text style={styles.brandTitle}>VINEYE</Text>
<Text style={styles.greetingText}>{t("home.greeting")}</Text>
</View>
<View style={styles.buttonsGroup}>
<TouchableOpacity
className="h-9 w-9 items-center justify-center rounded-full bg-neutral-200"
style={styles.notifButton}
activeOpacity={0.7}
onPress={() => navigation.navigate("Notifications")}
>
<Ionicons
name="notifications-outline"
size={20}
size={22}
color={colors.neutral[800]}
/>
<View style={styles.notifBadge} />
</TouchableOpacity>
<TouchableOpacity
className="h-9 w-9 items-center justify-center rounded-full bg-white border border-neutral-200"
style={styles.notifButton}
activeOpacity={0.7}
onPress={() => navigation.navigate("Settings")}
onPress={() => navigation.navigate("Profile")}
>
<Ionicons
name="settings-outline"
size={20}
color={colors.neutral[900]}
name="person-outline"
size={22}
color={colors.neutral[800]}
/>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
headerContainer: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 20,
paddingTop: 12,
paddingBottom: 8,
backgroundColor: "transparent",
},
textContainer: {
flex: 1,
},
brandTitle: {
fontSize: 32,
fontWeight: "900", // Très gras pour l'identité
color: colors.primary[900],
letterSpacing: -1, // Look "Logo"
},
greetingText: {
fontSize: 10,
fontWeight: "500",
color: colors.neutral[500],
marginTop: -2,
},
buttonsGroup: {
flexDirection: "row" as const,
backgroundColor: "#FFFFFF",
borderWidth: 1,
borderColor: "#F0F0F0",
borderRadius: 16,
},
notifButton: {
height: 48,
width: 48,
alignItems: "center",
justifyContent: "center",
borderRadius: 16,
backgroundColor: "#FFFFFF",
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.05,
shadowRadius: 10,
},
android: {
elevation: 3,
},
}),
},
notifBadge: {
position: "absolute",
top: 10,
right: 10,
width: 9,
height: 9,
borderRadius: 5,
backgroundColor: "#EF4444",
borderWidth: 1.5,
borderColor: "#FFFFFF",
},
});

View file

@ -0,0 +1,79 @@
import { View, TextInput, StyleSheet, TouchableOpacity } from "react-native";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import { colors } from "@/theme/colors";
export default function SearchSection() {
const { t } = useTranslation();
return (
<View style={styles.container}>
<View style={styles.searchWrapper}>
{/* Icône de recherche */}
<Ionicons
name="search"
size={20}
color={colors.neutral[400]}
style={styles.searchIcon}
/>
{/* Champ de saisie */}
<TextInput
style={styles.input}
placeholder={t("home.searchPlaceholder") ?? "Rechercher..."}
placeholderTextColor={colors.neutral[400]}
selectionColor={colors.primary[500]}
autoCorrect={false}
/>
{/* Optionnel: Petit séparateur + Icône Filtre pour le look Premium */}
<TouchableOpacity style={styles.filterButton} activeOpacity={0.7}>
<View style={styles.divider} />
<Ionicons name="options-outline" size={18} color={colors.primary[600]} />
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 20,
paddingBottom: 16,
paddingTop: 4,
},
searchWrapper: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#F5F7F9", // Un gris bleuté plus frais que neutral-200
borderRadius: 100, // On garde ton style "full"
paddingHorizontal: 16,
height: 52, // Hauteur standardisée pour le tactile
borderWidth: 1,
borderColor: "#EAECEF",
},
searchIcon: {
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
fontWeight: "500",
color: colors.neutral[900],
// Évite le décalage de texte sur Android
paddingVertical: 0,
height: "100%",
},
filterButton: {
flexDirection: "row",
alignItems: "center",
paddingLeft: 12,
},
divider: {
width: 1,
height: 20,
backgroundColor: "#E2E4E7",
marginRight: 12,
},
});

View file

@ -0,0 +1,104 @@
import { View, TouchableOpacity, StyleSheet } from "react-native";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
const CURRENT_ALERT = {
type: "warning" as const,
title: "Stay in the know",
message: "Get alerts on your money activity and budgets",
};
export default function SeasonAlert() {
const { t } = useTranslation();
return (
<View style={styles.container}>
<View style={styles.content}>
{/* Bouton Fermer */}
<TouchableOpacity style={styles.closeButton} activeOpacity={0.7}>
<Ionicons name="close" size={18} color={colors.primary[900]} />
</TouchableOpacity>
{/* Contenu Texte */}
<View style={styles.textContainer}>
<Text style={styles.title}>{t(CURRENT_ALERT.title)}</Text>
<Text style={styles.message}>{t(CURRENT_ALERT.message)}</Text>
</View>
{/* Bouton d'action */}
<TouchableOpacity style={styles.actionButton} activeOpacity={0.7}>
<Text style={styles.actionText}>{t("Allow notifications")}</Text>
</TouchableOpacity>
{/* Illustration decorative */}
<View style={styles.decoration}>
<Ionicons name="notifications" size={100} color={colors.primary[300]} />
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
marginHorizontal: 20,
marginBottom: 24,
borderRadius: 28,
backgroundColor: colors.primary[200],
overflow: "hidden",
},
content: {
position: "relative",
padding: 24,
},
closeButton: {
position: "absolute",
right: 16,
top: 16,
height: 32,
width: 32,
alignItems: "center",
justifyContent: "center",
borderRadius: 16,
backgroundColor: colors.primary[300],
zIndex: 1,
},
textContainer: {
paddingRight: 48,
},
title: {
fontSize: 22,
fontWeight: "700",
letterSpacing: -0.5,
color: colors.primary[900],
},
message: {
marginTop: 8,
fontSize: 16,
lineHeight: 24,
color: colors.primary[800],
opacity: 0.6,
},
actionButton: {
marginTop: 24,
alignSelf: "flex-start",
borderRadius: 12,
backgroundColor: colors.primary[400],
paddingHorizontal: 24,
paddingVertical: 12,
elevation: 2,
},
actionText: {
fontSize: 16,
fontWeight: "700",
color: colors.primary[900],
},
decoration: {
position: "absolute",
bottom: -16,
right: -16,
opacity: 0.8,
},
});

View file

@ -1,5 +1,6 @@
import { View, TouchableOpacity } from "react-native";
import { View, TouchableOpacity, StyleSheet } from "react-native";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
@ -12,21 +13,76 @@ export default function SectionHeader({
onViewAll?: () => void;
}) {
const { t } = useTranslation();
return (
<View className="flex-row items-center justify-between">
<Text className="text-[17px] font-semibold text-neutral-900">
<View style={styles.container}>
{/* Titre avec graisse plus affirmée */}
<Text style={styles.title}>
{title}
</Text>
{onViewAll && (
<TouchableOpacity onPress={onViewAll}>
<Text
className="text-[13px] font-medium"
style={{ color: colors.primary[700] }}
>
<TouchableOpacity
onPress={onViewAll}
activeOpacity={0.6}
style={styles.button}
>
<Text style={styles.buttonText}>
{t("common.viewAll") ?? "View all"}
</Text>
{/* Petit chevron discret pour guider l'œil */}
<View style={styles.iconWrapper}>
<Ionicons
name="chevron-forward"
size={12}
color={colors.primary[600]}
/>
</View>
</TouchableOpacity>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginBottom: 16, // Espace constant sous le header
paddingHorizontal: 4,
},
title: {
fontSize: 18,
fontWeight: "800", // Plus épais pour le style Bento
color: "#1A1A1A",
letterSpacing: -0.5, // Look moderne
},
button: {
flexDirection: "row",
alignItems: "center",
backgroundColor: `${colors.primary[50]}`, // Fond très léger
paddingVertical: 6,
paddingLeft: 12,
paddingRight: 6,
borderRadius: 12,
},
buttonText: {
fontSize: 13,
fontWeight: "700",
color: colors.primary[700],
marginRight: 4,
},
iconWrapper: {
backgroundColor: "#FFFFFF",
borderRadius: 6,
padding: 2,
// Légère ombre pour faire ressortir l'icône
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.05,
shadowRadius: 2,
elevation: 1,
},
});

129
VinEye/src/data/diseases.ts Normal file
View file

@ -0,0 +1,129 @@
export interface Disease {
id: string;
name: string;
type: "fungal" | "bacterial" | "pest" | "abiotic";
icon: string;
iconColor: string;
bgColor: string;
severity: "high" | "medium" | "low";
description: string;
symptoms: string[];
treatment: string;
season: string;
}
export const VINE_DISEASES: Disease[] = [
{
id: "mildiou",
name: "diseases.mildiou.name",
type: "fungal",
icon: "water-outline",
iconColor: "#BA7517",
bgColor: "#FAEEDA",
severity: "high",
description: "diseases.mildiou.description",
symptoms: [
"diseases.mildiou.symptom1",
"diseases.mildiou.symptom2",
"diseases.mildiou.symptom3",
],
treatment: "diseases.mildiou.treatment",
season: "diseases.mildiou.season",
},
{
id: "oidium",
name: "diseases.oidium.name",
type: "fungal",
icon: "snow-outline",
iconColor: "#534AB7",
bgColor: "#EEEDFE",
severity: "high",
description: "diseases.oidium.description",
symptoms: [
"diseases.oidium.symptom1",
"diseases.oidium.symptom2",
],
treatment: "diseases.oidium.treatment",
season: "diseases.oidium.season",
},
{
id: "black_rot",
name: "diseases.blackRot.name",
type: "fungal",
icon: "ellipse",
iconColor: "#5F5E5A",
bgColor: "#F1EFE8",
severity: "high",
description: "diseases.blackRot.description",
symptoms: [
"diseases.blackRot.symptom1",
"diseases.blackRot.symptom2",
],
treatment: "diseases.blackRot.treatment",
season: "diseases.blackRot.season",
},
{
id: "esca",
name: "diseases.esca.name",
type: "fungal",
icon: "leaf-outline",
iconColor: "#993C1D",
bgColor: "#FAECE7",
severity: "medium",
description: "diseases.esca.description",
symptoms: [
"diseases.esca.symptom1",
"diseases.esca.symptom2",
],
treatment: "diseases.esca.treatment",
season: "diseases.esca.season",
},
{
id: "botrytis",
name: "diseases.botrytis.name",
type: "fungal",
icon: "cloud-outline",
iconColor: "#185FA5",
bgColor: "#E6F1FB",
severity: "medium",
description: "diseases.botrytis.description",
symptoms: [
"diseases.botrytis.symptom1",
"diseases.botrytis.symptom2",
],
treatment: "diseases.botrytis.treatment",
season: "diseases.botrytis.season",
},
{
id: "flavescence_doree",
name: "diseases.flavescence.name",
type: "bacterial",
icon: "warning-outline",
iconColor: "#A32D2D",
bgColor: "#FCEBEB",
severity: "high",
description: "diseases.flavescence.description",
symptoms: [
"diseases.flavescence.symptom1",
"diseases.flavescence.symptom2",
],
treatment: "diseases.flavescence.treatment",
season: "diseases.flavescence.season",
},
{
id: "chlorose",
name: "diseases.chlorose.name",
type: "abiotic",
icon: "sunny-outline",
iconColor: "#639922",
bgColor: "#EAF3DE",
severity: "low",
description: "diseases.chlorose.description",
symptoms: [
"diseases.chlorose.symptom1",
"diseases.chlorose.symptom2",
],
treatment: "diseases.chlorose.treatment",
season: "diseases.chlorose.season",
},
];

35
VinEye/src/data/guides.ts Normal file
View file

@ -0,0 +1,35 @@
export interface Guide {
id: string;
title: string;
subtitle: string;
icon: string;
iconColor: string;
bgColor: string;
}
export const PRACTICAL_GUIDES: Guide[] = [
{
id: "healthy_leaf",
title: "guides.healthyLeaf.title",
subtitle: "guides.healthyLeaf.subtitle",
icon: "happy-outline",
iconColor: "#1D9E75",
bgColor: "#E1F5EE",
},
{
id: "treatment_calendar",
title: "guides.treatmentCalendar.title",
subtitle: "guides.treatmentCalendar.subtitle",
icon: "book-outline",
iconColor: "#185FA5",
bgColor: "#E6F1FB",
},
{
id: "grape_varieties",
title: "guides.grapeVarieties.title",
subtitle: "guides.grapeVarieties.subtitle",
icon: "wine-outline",
iconColor: "#534AB7",
bgColor: "#EEEDFE",
},
];

View file

@ -12,11 +12,13 @@
"retry": "Retry",
"map": "Map",
"notifications": "Notifications",
"settings": "Settings"
"settings": "Settings",
"details": "Details"
},
"home": {
"greeting": "Hello, Winemaker!",
"scanButton": "Scan a vine",
"searchPlaceholder": "Search a disease, a grape variety...",
"totalScans": "Total scans",
"uniqueGrapes": "Grapes found",
"currentStreak": "Current streak",
@ -27,7 +29,142 @@
"bannerButton": "Get started",
"lastScan": "Last scan",
"noScansYet": "No scans yet",
"startScanning": "Start scanning!"
"startScanning": "Start scanning!",
"tapToStart": "Tap to scan",
"frequentDiseases": "Frequent diseases",
"seasonAlert": {
"title": "High downy mildew risk",
"message": "Rain and heat expected this week. Keep an eye on your leaves."
},
"practicalGuides": "Practical guides"
},
"diseases": {
"types": {
"fungal": "Fungal",
"bacterial": "Bacterial",
"pest": "Pest",
"abiotic": "Deficiency"
},
"mildiou": {
"name": "Downy mildew",
"description": "Downy mildew is caused by the fungus Plasmopara viticola. It attacks all green parts of the vine, mainly the leaves.",
"symptom1": "Oily yellow spots on the upper surface of leaves",
"symptom2": "White cottony down on the underside",
"symptom3": "Drying and premature leaf drop",
"treatment": "Preventive copper-based treatment (Bordeaux mixture). Apply before rain, renew every 10-14 days.",
"season": "May to August — favored by heat and humidity"
},
"oidium": {
"name": "Powdery mildew",
"description": "Powdery mildew is caused by Erysiphe necator. It develops in warm, dry weather, unlike downy mildew.",
"symptom1": "White-grey powder on leaves and clusters",
"symptom2": "Berries that crack or dry out",
"treatment": "Sulfur dusting or spraying. Preventive treatments from bud break.",
"season": "April to September — favored by warm, dry weather"
},
"blackRot": {
"name": "Black rot",
"description": "Black rot is caused by Guignardia bidwellii. It causes significant damage to berries.",
"symptom1": "Circular brown spots bordered with black on leaves",
"symptom2": "Mummified, black and wrinkled berries",
"treatment": "Remove mummified berries. Preventive fungicide treatments in spring.",
"season": "May to July — favored by spring rains"
},
"esca": {
"name": "Esca",
"description": "Esca is a complex of wood diseases caused by several fungi. A chronic disease that can kill the vine.",
"symptom1": "Discoloration between leaf veins (striped appearance)",
"symptom2": "Sudden drying of foliage (apoplexy)",
"treatment": "No curative treatment. Cutting back affected vine. Protect pruning wounds.",
"season": "Symptoms visible in summer — June to September"
},
"botrytis": {
"name": "Botrytis",
"description": "Grey rot is caused by Botrytis cinerea. It attacks clusters at maturity.",
"symptom1": "Soft grey rot on berries",
"symptom2": "Characteristic grey felt on clusters",
"treatment": "Promote cluster aeration. Leaf removal. Anti-botrytis treatments before cluster closure.",
"season": "August to harvest — favored by humidity"
},
"flavescence": {
"name": "Flavescence dorée",
"description": "Phytoplasma disease transmitted by the leafhopper Scaphoideus titanus. Regulated disease, mandatory reporting.",
"symptom1": "Leaf rolling with yellow or red coloration depending on variety",
"symptom2": "Non-lignification of shoots (remain rubbery)",
"treatment": "Mandatory uprooting of contaminated vines. Insecticide treatment against the vector leafhopper.",
"season": "Symptoms visible from July"
},
"chlorose": {
"name": "Iron chlorosis",
"description": "Leaf yellowing due to iron deficiency, often linked to overly calcareous soil.",
"symptom1": "Yellowing between veins, veins remaining green",
"symptom2": "General weakening of the vine",
"treatment": "Iron chelate application. Choose rootstock adapted to calcareous soils.",
"season": "Spring — especially on calcareous soils after heavy rain"
}
},
"notifications": {
"markAllRead": "All read",
"empty": {
"title": "Nothing new",
"body": "Your notifications will appear here. Scan a vine to get started!"
},
"mock": {
"mildewAlert": {
"title": "Mildew Alert",
"body": "Favorable conditions for downy mildew detected in your area. Watch for yellow spots on leaves."
},
"sulfurTip": {
"title": "Tip: Sulfur Treatment",
"body": "Now is a good time for preventive sulfur dusting against powdery mildew."
},
"scanReminder": {
"title": "Scan Reminder",
"body": "You haven't scanned in 3 days. Keep your streak alive!"
},
"botrytisAlert": {
"title": "Botrytis Risk",
"body": "High humidity favors grey rot. Consider aerating your clusters."
},
"pruningTip": {
"title": "Tip: Spring Pruning",
"body": "Protect pruning wounds with a sealing paste to prevent esca."
},
"updateAvailable": {
"title": "Update Available",
"body": "VinEye v2.1 is available with detection for 3 new diseases."
}
}
},
"library": {
"title": "My Library",
"plants": "plants",
"empty": {
"title": "No scanned plants",
"body": "Scan your first vine to start your collection!"
}
},
"guides": {
"screenTitle": "Guides & Tips",
"tabDiseases": "Diseases",
"tabGuides": "Practical Guides",
"severity": {
"critical": "Critical",
"moderate": "Moderate",
"low": "Low"
},
"healthyLeaf": {
"title": "Recognizing a healthy leaf",
"subtitle": "Basics for beginners"
},
"treatmentCalendar": {
"title": "Treatment calendar",
"subtitle": "When and how to treat"
},
"grapeVarieties": {
"title": "Bordeaux grape varieties",
"subtitle": "Merlot, Cabernet, Sauvignon..."
}
},
"scanner": {
"scanning": "Analyzing...",
@ -82,7 +219,21 @@
"language": "Language",
"resetData": "Reset data",
"resetConfirm": "Are you sure you want to reset all data?",
"days": "days"
"days": "days",
"xpTotal": "Total XP"
},
"settings": {
"general": "General",
"app": "Application",
"editProfile": "Edit profile",
"privacy": "Privacy",
"premiumStatus": "Premium Status",
"inactive": "Inactive",
"appearance": "Appearance",
"helpCenter": "Help Center",
"terms": "Terms of Use",
"referTitle": "Refer a friend",
"referBody": "Share VinEye and earn bonus XP for every friend you invite."
},
"achievements": {
"firstScan": "First Scan",

View file

@ -12,11 +12,13 @@
"retry": "Réessayer",
"map": "Carte",
"notifications": "Notifications",
"settings": "Paramètres"
"settings": "Paramètres",
"details": "Détails"
},
"home": {
"greeting": "Bonjour, Vigneron !",
"scanButton": "Scanner une vigne",
"searchPlaceholder": "Rechercher une maladie, un cépage...",
"totalScans": "Scans totaux",
"uniqueGrapes": "Cépages trouvés",
"currentStreak": "Streak actuel",
@ -27,7 +29,142 @@
"bannerButton": "Commencer",
"lastScan": "Dernier scan",
"noScansYet": "Aucun scan pour l'instant",
"startScanning": "Commencez à scanner !"
"startScanning": "Commencez à scanner !",
"tapToStart": "Appuyez pour scanner",
"frequentDiseases": "Maladies fréquentes",
"seasonAlert": {
"title": "Risque mildiou élevé",
"message": "Pluie et chaleur prévues cette semaine. Surveillez vos feuilles."
},
"practicalGuides": "Guides pratiques"
},
"diseases": {
"types": {
"fungal": "Fongique",
"bacterial": "Bactérien",
"pest": "Ravageur",
"abiotic": "Carence"
},
"mildiou": {
"name": "Mildiou",
"description": "Le mildiou est causé par le champignon Plasmopara viticola. Il attaque toutes les parties vertes de la vigne, principalement les feuilles.",
"symptom1": "Taches jaunes huileuses sur la face supérieure des feuilles",
"symptom2": "Duvet blanc cotonneux sur la face inférieure",
"symptom3": "Dessèchement et chute prématurée des feuilles",
"treatment": "Traitement préventif à base de cuivre (bouillie bordelaise). Appliquer avant les pluies, renouveler tous les 10-14 jours.",
"season": "Mai à août — favorisé par la chaleur et l'humidité"
},
"oidium": {
"name": "Oïdium",
"description": "L'oïdium est causé par Erysiphe necator. Il se développe par temps chaud et sec, contrairement au mildiou.",
"symptom1": "Poudre blanche-grisâtre sur feuilles et grappes",
"symptom2": "Baies qui éclatent ou se dessèchent",
"treatment": "Soufre en poudrage ou pulvérisation. Traitements préventifs dès le débourrement.",
"season": "Avril à septembre — favorisé par temps chaud et sec"
},
"blackRot": {
"name": "Black rot",
"description": "Le black rot est causé par Guignardia bidwellii. Il provoque des dégâts importants sur les baies.",
"symptom1": "Taches brunes circulaires bordées de noir sur les feuilles",
"symptom2": "Baies momifiées, noires et ridées",
"treatment": "Éliminer les baies momifiées. Traitements fongicides préventifs au printemps.",
"season": "Mai à juillet — favorisé par les pluies printanières"
},
"esca": {
"name": "Esca",
"description": "L'esca est un complexe de maladies du bois causé par plusieurs champignons. Maladie chronique qui peut tuer le cep.",
"symptom1": "Décolorations entre les nervures des feuilles (aspect tigré)",
"symptom2": "Dessèchement brutal du feuillage (apoplexie)",
"treatment": "Aucun traitement curatif. Recépage du cep atteint. Protéger les plaies de taille.",
"season": "Symptômes visibles en été — juin à septembre"
},
"botrytis": {
"name": "Botrytis",
"description": "La pourriture grise est causée par Botrytis cinerea. Elle attaque les grappes à maturité.",
"symptom1": "Pourriture molle grise sur les baies",
"symptom2": "Feutrage gris caractéristique sur les grappes",
"treatment": "Favoriser l'aération des grappes. Effeuillage. Traitements anti-botrytis avant fermeture de la grappe.",
"season": "Août à vendanges — favorisé par l'humidité"
},
"flavescence": {
"name": "Flavescence dorée",
"description": "Maladie à phytoplasme transmise par la cicadelle Scaphoideus titanus. Maladie réglementée, déclaration obligatoire.",
"symptom1": "Enroulement des feuilles avec coloration jaune ou rouge selon le cépage",
"symptom2": "Non-aoûtement des rameaux (restent caoutchouteux)",
"treatment": "Arrachage obligatoire des ceps contaminés. Traitement insecticide contre la cicadelle vectrice.",
"season": "Symptômes visibles à partir de juillet"
},
"chlorose": {
"name": "Chlorose ferrique",
"description": "Jaunissement des feuilles dû à une carence en fer, souvent lié à un sol trop calcaire.",
"symptom1": "Jaunissement entre les nervures, nervures restant vertes",
"symptom2": "Affaiblissement général de la vigne",
"treatment": "Apport de chélates de fer. Choix d'un porte-greffe adapté aux sols calcaires.",
"season": "Printemps — surtout sur sols calcaires après de fortes pluies"
}
},
"notifications": {
"markAllRead": "Tout lu",
"empty": {
"title": "Rien de nouveau",
"body": "Vos notifications apparaîtront ici. Scannez une vigne pour commencer !"
},
"mock": {
"mildewAlert": {
"title": "Alerte Mildiou",
"body": "Conditions favorables au mildiou détectées dans votre zone. Surveillez les taches jaunes sur les feuilles."
},
"sulfurTip": {
"title": "Conseil : Traitement soufre",
"body": "C'est le bon moment pour un poudrage de soufre préventif contre l'oïdium."
},
"scanReminder": {
"title": "Rappel de scan",
"body": "Vous n'avez pas scanné depuis 3 jours. Gardez votre streak en vie !"
},
"botrytisAlert": {
"title": "Risque Botrytis",
"body": "L'humidité élevée favorise la pourriture grise. Pensez à aérer vos grappes."
},
"pruningTip": {
"title": "Conseil : Taille de printemps",
"body": "Protégez vos plaies de taille avec un mastic cicatrisant pour prévenir l'esca."
},
"updateAvailable": {
"title": "Mise à jour disponible",
"body": "VinEye v2.1 est disponible avec la détection de 3 nouvelles maladies."
}
}
},
"library": {
"title": "Ma bibliothèque",
"plants": "plantes",
"empty": {
"title": "Aucune plante scannée",
"body": "Scannez votre première vigne pour commencer votre collection !"
}
},
"guides": {
"screenTitle": "Guides & Conseils",
"tabDiseases": "Maladies",
"tabGuides": "Guides Pratiques",
"severity": {
"critical": "Critique",
"moderate": "Modéré",
"low": "Faible"
},
"healthyLeaf": {
"title": "Reconnaître une feuille saine",
"subtitle": "Les bases pour débutants"
},
"treatmentCalendar": {
"title": "Calendrier de traitement",
"subtitle": "Quand et comment traiter"
},
"grapeVarieties": {
"title": "Les cépages bordelais",
"subtitle": "Merlot, Cabernet, Sauvignon..."
}
},
"scanner": {
"scanning": "Analyse en cours...",
@ -82,7 +219,21 @@
"language": "Langue",
"resetData": "Réinitialiser les données",
"resetConfirm": "Êtes-vous sûr de vouloir réinitialiser toutes les données ?",
"days": "jours"
"days": "jours",
"xpTotal": "XP total"
},
"settings": {
"general": "Général",
"app": "Application",
"editProfile": "Modifier le profil",
"privacy": "Confidentialité",
"premiumStatus": "Statut Premium",
"inactive": "Inactif",
"appearance": "Apparence",
"helpCenter": "Centre d'aide",
"terms": "Conditions d'utilisation",
"referTitle": "Inviter un ami",
"referBody": "Partagez VinEye et gagnez des XP bonus pour chaque ami invité."
},
"achievements": {
"firstScan": "Premier Scan",

View file

@ -4,18 +4,22 @@ import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
import * as Haptics from "expo-haptics";
import { House, ScanLine, MapPin } from "lucide-react-native";
import { House, ScanLine, Map, BookOpen, Leaf } from "lucide-react-native";
import HomeScreen from "@/screens/HomeScreen";
import ScannerScreen from "@/screens/ScannerScreen";
import MapScreen from "@/screens/MapScreen";
import GuidesScreen from "@/screens/GuidesScreen";
import LibraryScreen from "@/screens/LibraryScreen";
import { colors } from "@/theme/colors";
const Tab = createBottomTabNavigator();
const TAB_ICONS: Record<string, any> = {
Home: House,
Map: MapPin,
Guides: BookOpen,
Library: Leaf,
Map: Map,
};
function MyCustomTabBar({ state, descriptors, navigation }: any) {
@ -118,7 +122,7 @@ function MyCustomTabBar({ state, descriptors, navigation }: any) {
strokeWidth={isFocused ? 2.5 : 1.8}
/>
)}
<Text
{/* <Text
numberOfLines={1}
style={{
fontSize: 11,
@ -128,7 +132,7 @@ function MyCustomTabBar({ state, descriptors, navigation }: any) {
}}
>
{label}
</Text>
</Text> */}
</TouchableOpacity>
);
})}
@ -149,11 +153,21 @@ export default function BottomTabNavigator() {
component={HomeScreen}
options={{ tabBarLabel: t("common.home") }}
/>
<Tab.Screen
name="Guides"
component={GuidesScreen}
options={{ tabBarLabel: t("guides.screenTitle") }}
/>
<Tab.Screen
name="Scanner"
component={ScannerScreen}
options={{ tabBarLabel: t("common.scan") }}
/>
<Tab.Screen
name="Library"
component={LibraryScreen}
options={{ tabBarLabel: t("library.title") }}
/>
<Tab.Screen
name="Map"
component={MapScreen}

View file

@ -3,8 +3,11 @@ import { NavigationContainer } from '@react-navigation/native';
import SplashScreen from '@/screens/SplashScreen';
import ResultScreen from '@/screens/ResultScreen';
import HistoryScreen from '@/screens/HistoryScreen';
import NotificationsScreen from '@/screens/NotificationsScreen';
import ProfileScreen from '@/screens/ProfileScreen';
import SettingsScreen from '@/screens/SettingsScreen';
import GuidesScreen from '@/screens/GuidesScreen';
import LibraryScreen from '@/screens/LibraryScreen';
import BottomTabNavigator from './BottomTabNavigator';
import linking from './linking';
import type { RootStackParamList } from '@/types/navigation';
@ -27,12 +30,27 @@ export default function RootNavigator() {
/>
<Stack.Screen
name="Notifications"
component={HistoryScreen}
component={NotificationsScreen}
options={{ animation: 'slide_from_right' }}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{ animation: 'slide_from_right' }}
/>
<Stack.Screen
name="Settings"
component={ProfileScreen}
component={SettingsScreen}
options={{ animation: 'slide_from_right' }}
/>
<Stack.Screen
name="Guides"
component={GuidesScreen}
options={{ animation: 'slide_from_right' }}
/>
<Stack.Screen
name="Library"
component={LibraryScreen}
options={{ animation: 'slide_from_right' }}
/>
</Stack.Navigator>

View file

@ -15,7 +15,10 @@ const linking: LinkingOptions<RootStackParamList> = {
},
Result: 'result',
Notifications: 'notifications',
Profile: 'profile',
Settings: 'settings',
Guides: 'guides',
Library: 'library',
},
},
};

View file

@ -0,0 +1,373 @@
import { useState } from "react";
import {
View,
FlatList,
TouchableOpacity,
StyleSheet,
Platform,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
import { VINE_DISEASES } from "@/data/diseases";
import { PRACTICAL_GUIDES } from "@/data/guides";
import type { Disease } from "@/data/diseases";
import type { Guide } from "@/data/guides";
type Tab = "diseases" | "guides";
const DISEASE_TYPE_KEYS: Record<Disease["type"], string> = {
fungal: "diseases.types.fungal",
bacterial: "diseases.types.bacterial",
pest: "diseases.types.pest",
abiotic: "diseases.types.abiotic",
};
const SEVERITY_CONFIG: Record<
Disease["severity"],
{ label: string; color: string; bg: string }
> = {
high: { label: "guides.severity.critical", color: "#DC2626", bg: "#FEF2F2" },
medium: { label: "guides.severity.moderate", color: "#F59E0B", bg: "#FFFBEB" },
low: { label: "guides.severity.low", color: "#10B981", bg: "#ECFDF5" },
};
function DiseaseCard({ item }: { item: Disease }) {
const { t } = useTranslation();
const severity = SEVERITY_CONFIG[item.severity];
return (
<TouchableOpacity activeOpacity={0.7} style={styles.diseaseCard}>
{/* Image placeholder */}
<View style={[styles.diseaseBanner, { backgroundColor: item.bgColor }]}>
<Ionicons name={item.icon as any} size={36} color={item.iconColor} />
{/* Severity badge */}
<View style={[styles.severityBadge, { backgroundColor: severity.bg }]}>
<View style={[styles.severityDot, { backgroundColor: severity.color }]} />
<Text style={[styles.severityText, { color: severity.color }]}>
{t(severity.label)}
</Text>
</View>
</View>
{/* Content */}
<View style={styles.diseaseContent}>
<View style={[styles.typePill, { backgroundColor: `${item.iconColor}12` }]}>
<Text style={[styles.typeText, { color: item.iconColor }]}>
{t(DISEASE_TYPE_KEYS[item.type])}
</Text>
</View>
<Text style={styles.diseaseName} numberOfLines={1}>
{t(item.name)}
</Text>
<Text style={styles.diseaseSeason} numberOfLines={1}>
{t(item.season)}
</Text>
</View>
</TouchableOpacity>
);
}
function GuideCard({ item }: { item: Guide }) {
const { t } = useTranslation();
return (
<TouchableOpacity activeOpacity={0.7} style={styles.guideCard}>
<View style={[styles.guideIcon, { backgroundColor: `${item.iconColor}12` }]}>
<Ionicons name={item.icon as any} size={24} color={item.iconColor} />
</View>
<View style={styles.guideText}>
<Text style={styles.guideTitle} numberOfLines={1}>
{t(item.title)}
</Text>
<Text style={styles.guideSubtitle} numberOfLines={1}>
{t(item.subtitle)}
</Text>
</View>
<View style={styles.chevronWrap}>
<Ionicons name="chevron-forward" size={14} color="#D1D1D6" />
</View>
</TouchableOpacity>
);
}
export default function GuidesScreen() {
const { t } = useTranslation();
const navigation = useNavigation();
const [activeTab, setActiveTab] = useState<Tab>("diseases");
function handleBack() {
if (navigation.canGoBack()) {
navigation.goBack();
} else {
(navigation as any).navigate("Main");
}
}
return (
<SafeAreaView style={styles.safe} edges={["top"]}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity onPress={handleBack} style={styles.backBtn}>
<Ionicons name="chevron-back" size={24} color="#1A1A1A" />
</TouchableOpacity>
<Text style={styles.headerTitle}>{t("guides.screenTitle")}</Text>
<View style={{ width: 44 }} />
</View>
{/* Segmented Control */}
<View style={styles.tabContainer}>
<View style={styles.tabBar}>
<TouchableOpacity
style={[styles.tab, activeTab === "diseases" && styles.tabActive]}
onPress={() => setActiveTab("diseases")}
activeOpacity={0.7}
>
<Text
style={[
styles.tabText,
activeTab === "diseases" && styles.tabTextActive,
]}
>
{t("guides.tabDiseases")}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, activeTab === "guides" && styles.tabActive]}
onPress={() => setActiveTab("guides")}
activeOpacity={0.7}
>
<Text
style={[
styles.tabText,
activeTab === "guides" && styles.tabTextActive,
]}
>
{t("guides.tabGuides")}
</Text>
</TouchableOpacity>
</View>
</View>
{/* Content */}
{activeTab === "diseases" ? (
<FlatList
data={VINE_DISEASES}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <DiseaseCard item={item} />}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={{ height: 12 }} />}
/>
) : (
<FlatList
data={PRACTICAL_GUIDES}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <GuideCard item={item} />}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={{ height: 12 }} />}
/>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: {
flex: 1,
backgroundColor: "#F8F9FB",
},
// Header
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
paddingVertical: 10,
backgroundColor: "transparent",
},
backBtn: {
width: 44,
height: 44,
alignItems: "center",
justifyContent: "center",
borderRadius: 14,
backgroundColor: "#FFFFFF",
borderWidth: 1,
borderColor: "#F0F0F0",
},
headerTitle: {
fontSize: 18,
fontWeight: "600",
color: "#1A1A1A",
letterSpacing: -0.4,
},
// Tabs
tabContainer: {
paddingHorizontal: 20,
paddingBottom: 12,
},
tabBar: {
flexDirection: "row",
backgroundColor: "#EEEFF1",
borderRadius: 14,
padding: 4,
},
tab: {
flex: 1,
paddingVertical: 10,
alignItems: "center",
borderRadius: 11,
},
tabActive: {
backgroundColor: "#FFFFFF",
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 4,
},
android: { elevation: 2 },
}),
},
tabText: {
fontSize: 14,
fontWeight: "500",
color: "#8E8E93",
},
tabTextActive: {
color: "#1A1A1A",
fontWeight: "600",
},
// List
listContent: {
padding: 20,
paddingBottom: 40,
},
// Disease card
diseaseCard: {
backgroundColor: "#FFFFFF",
borderRadius: 24,
overflow: "hidden",
borderWidth: 1,
borderColor: "#F0F0F0",
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.04,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
},
diseaseBanner: {
height: 120,
alignItems: "center",
justifyContent: "center",
position: "relative",
},
severityBadge: {
position: "absolute",
top: 12,
right: 12,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 20,
gap: 5,
},
severityDot: {
width: 6,
height: 6,
borderRadius: 3,
},
severityText: {
fontSize: 11,
fontWeight: "600",
},
diseaseContent: {
padding: 16,
},
typePill: {
alignSelf: "flex-start",
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
marginBottom: 8,
},
typeText: {
fontSize: 11,
fontWeight: "600",
},
diseaseName: {
fontSize: 16,
fontWeight: "500",
color: "#1A1A1A",
marginBottom: 4,
},
diseaseSeason: {
fontSize: 12,
fontWeight: "400",
color: "#8E8E93",
},
// Guide card
guideCard: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "#FFFFFF",
borderRadius: 24,
padding: 14,
borderWidth: 1,
borderColor: "#F0F0F0",
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.04,
shadowRadius: 8,
},
android: { elevation: 2 },
}),
},
guideIcon: {
width: 52,
height: 52,
borderRadius: 18,
alignItems: "center",
justifyContent: "center",
},
guideText: {
flex: 1,
marginLeft: 14,
},
guideTitle: {
fontSize: 15,
fontWeight: "500",
color: "#1A1A1A",
marginBottom: 2,
},
guideSubtitle: {
fontSize: 13,
fontWeight: "400",
color: "#8E8E93",
},
chevronWrap: {
marginLeft: 8,
backgroundColor: "#F8F9FA",
padding: 6,
borderRadius: 12,
},
});

View file

@ -1,188 +1,59 @@
import { useEffect } from "react";
import { View, ScrollView, TouchableOpacity, TextInput } from "react-native";
import { View, ScrollView } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withSequence,
withTiming,
} from "react-native-reanimated";
import { Text } from "@/components/ui/text";
import { ProgressRing } from "@/components/gamification/ProgressRing";
import { ScanCard } from "@/components/history/ScanCard";
import { useGameProgress } from "@/hooks/useGameProgress";
import { useHistory } from "@/hooks/useHistory";
import { colors } from "@/theme/colors";
import {
getLevelForXP,
getLevelNumber,
getXPProgress,
} from "@/utils/achievements";
import type { RootStackParamList } from "@/types/navigation";
import StatCard from "@/components/home/gamificationstat";
import StatisticsSection from "@/components/home/statssection";
import SearchHeader from "@/components/home/SearchHeader";
import SearchSection from "@/components/home/SearchSection";
import SectionHeader from "@/components/home/components/homeheader";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
import FrequentDiseases from "@/components/home/FrequentDiseases";
import SeasonAlert from "@/components/home/SeasonAlert";
import PracticalGuides from "@/components/home/PracticalGuides";
import HeroScanner from "@/components/home/HomeCta";
type HomeNav = NativeStackNavigationProp<RootStackParamList>;
interface GameProgress {
totalScans: number;
uniqueGrapes: string[];
streak: number;
}
const userProgress = {
streak: 12, // La série de jours
xpTotal: 2450, // Le total de points XP
// ... tes autres données
};
const STAT_CARDS: {
labelKey: string;
icon: keyof typeof Ionicons.glyphMap;
bg: string;
accent: string;
dark: string;
getValue: (p: GameProgress) => number;
}[] = [
{
labelKey: "home.totalScans",
icon: "scan-outline",
bg: "#E8F0EA",
accent: "#2D6A4F",
dark: "#1B4332",
getValue: (p) => p.totalScans,
},
{
labelKey: "home.uniqueGrapes",
icon: "leaf-outline",
bg: "#EBE5F6",
accent: "#7B5EA7",
dark: "#3E0047",
getValue: (p) => p.uniqueGrapes.length,
},
{
labelKey: "home.currentStreak",
icon: "flame-outline",
bg: "#F0EBE3",
accent: "#8B7355",
dark: "#4A3F30",
getValue: (p) => p.streak,
},
];
type Nav = NativeStackNavigationProp<RootStackParamList>;
export default function HomeScreen() {
const { t } = useTranslation();
const navigation = useNavigation<HomeNav>();
const { progress } = useGameProgress();
const { history } = useHistory();
const pulse = useSharedValue(1);
useEffect(() => {
pulse.value = withRepeat(
withSequence(
withTiming(1.04, { duration: 1200 }),
withTiming(1, { duration: 1200 }),
),
-1,
false,
);
}, []);
const pulseStyle = useAnimatedStyle(() => ({
transform: [{ scale: pulse.value }],
}));
const lastScan = history[0];
const currentLevel = getLevelForXP(progress.xp);
const levelNumber = getLevelNumber(progress.xp);
const {
current: xpInLevel,
total: xpTotal,
ratio: xpRatio,
} = getXPProgress(progress.xp);
const navigation = useNavigation<Nav>();
return (
<SafeAreaView className="flex-1 bg-[#FAFAFA]" edges={["top"]}>
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentContainerStyle={{
paddingBottom: 24,
}}
contentContainerStyle={{ paddingBottom: 24 }}
>
<SearchHeader />
<StatisticsSection progress={userProgress} />
<SearchSection />
{/* Scan banner */}
<View
className="mx-5 mb-6 rounded-2xl px-5 pt-5 pb-4 shadow-sm overflow-hidden relative border border-gray-50"
style={{ backgroundColor: colors.primary[100] }}
>
{/* Decorative leaf top-right */}
<View className="absolute -top-1 -right-1 opacity-30">
<Ionicons name="leaf" size={80} color={colors.primary[600]} />
<HeroScanner />
{/* Frequent diseases carousel */}
<View className="mb-6 gap-3">
<View className="px-5">
<SectionHeader
title={t("home.frequentDiseases")}
onViewAll={() => navigation.navigate("Guides")}
/>
</View>
<Text
className="mb-1 text-[18px] font-bold"
style={{ color: colors.primary[900] }}
>
{t("home.bannerTitle")}
</Text>
<Text
className="mb-5 max-w-[220px] text-[13px] leading-[18px]"
style={{ color: colors.primary[700] }}
>
{t("home.bannerSubtitle")}
</Text>
{/* Scan icon centered */}
<View className="mb-5 items-center">
<Animated.View style={pulseStyle}>
<View
className="h-20 w-20 items-center justify-center rounded-full"
style={{ backgroundColor: colors.primary[200] }}
>
<Ionicons name="scan" size={36} color={colors.primary[800]} />
</View>
</Animated.View>
</View>
{/* Full-width scan button */}
<TouchableOpacity
activeOpacity={0.8}
className="flex-row items-center justify-center gap-2 rounded-full py-3"
style={{ backgroundColor: colors.primary[800] }}
onPress={() => navigation.navigate("Scanner")}
>
<Text className="text-[15px] font-semibold text-white">
{t("home.scanButton")}
</Text>
<MaterialIcons name="arrow-forward-ios" size={16} color="white" />
</TouchableOpacity>
<FrequentDiseases />
</View>
{/* Last scan section */}
{lastScan && (
<View className="mx-5 mb-6 gap-2">
<SectionHeader
title={t("home.lastScan")}
onViewAll={() => navigation.navigate("Notifications")}
/>
<ScanCard record={lastScan} />
</View>
)}
{/* Season alert */}
<SeasonAlert />
{/* Practical guides */}
<View className="mx-5 mb-6 gap-3">
<SectionHeader
title={t("home.practicalGuides")}
onViewAll={() => navigation.navigate("Guides")}
/>
<PracticalGuides />
</View>
<View className="h-8" />
</ScrollView>

View file

@ -0,0 +1,296 @@
import { useState } from "react";
import {
View,
FlatList,
TouchableOpacity,
StyleSheet,
Platform,
Dimensions,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
import SearchHeader from "@/components/home/SearchHeader";
import SearchSection from "@/components/home/SearchSection";
// ─── Types ───────────────────────────────────────────────
interface ScannedPlant {
id: string;
name: string;
date: string;
color: string;
iconColor: string;
favorite: boolean;
}
// ─── Mock Data ───────────────────────────────────────────
const INITIAL_PLANTS: ScannedPlant[] = [
{
id: "1",
name: "Merlot",
date: "2026-04-01",
color: "#E9F5EC",
iconColor: colors.primary[700],
favorite: true,
},
{
id: "2",
name: "Cabernet Sauvignon",
date: "2026-03-28",
color: "#EEEDFE",
iconColor: "#534AB7",
favorite: false,
},
{
id: "3",
name: "Chardonnay",
date: "2026-03-25",
color: "#FAEEDA",
iconColor: "#BA7517",
favorite: true,
},
{
id: "4",
name: "Pinot Noir",
date: "2026-03-20",
color: "#FAECE7",
iconColor: "#993C1D",
favorite: false,
},
{
id: "5",
name: "Sauvignon Blanc",
date: "2026-03-15",
color: "#E6F1FB",
iconColor: "#185FA5",
favorite: false,
},
{
id: "6",
name: "Grenache",
date: "2026-03-10",
color: "#FCEBEB",
iconColor: "#A32D2D",
favorite: true,
},
];
const { width } = Dimensions.get("window");
const CARD_WIDTH = (width - 56) / 2;
// ─── Component ───────────────────────────────────────────
export default function LibraryScreen() {
const { t } = useTranslation();
const [plants, setPlants] = useState(INITIAL_PLANTS);
function toggleFavorite(id: string) {
setPlants((prev) =>
prev.map((p) => (p.id === id ? { ...p, favorite: !p.favorite } : p)),
);
}
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
return date.toLocaleDateString(undefined, {
day: "numeric",
month: "short",
year: "numeric",
});
}
const renderItem = ({ item }: { item: ScannedPlant }) => (
<View style={styles.card}>
{/* Image placeholder */}
<View style={[styles.imagePlaceholder, { backgroundColor: item.color }]}>
<Ionicons name="leaf" size={32} color={item.iconColor} />
<TouchableOpacity
onPress={() => toggleFavorite(item.id)}
style={styles.heartBtn}
activeOpacity={0.7}
>
<Ionicons
name={item.favorite ? "heart" : "heart-outline"}
size={18}
color={item.favorite ? "#EF4444" : "#C7C7CC"}
/>
</TouchableOpacity>
</View>
{/* Info */}
<View style={styles.cardInfo}>
<Text style={styles.plantName} numberOfLines={1}>
{item.name}
</Text>
<Text style={styles.plantDate}>{formatDate(item.date)}</Text>
</View>
</View>
);
const renderEmpty = () => (
<View style={styles.emptyContainer}>
<View style={styles.emptyIcon}>
<Ionicons name="leaf-outline" size={48} color={colors.neutral[300]} />
</View>
<Text style={styles.emptyTitle}>{t("library.empty.title")}</Text>
<Text style={styles.emptyBody}>{t("library.empty.body")}</Text>
</View>
);
return (
<SafeAreaView style={styles.safe} edges={["top"]}>
<FlatList
data={plants}
keyExtractor={(item) => item.id}
numColumns={2}
columnWrapperStyle={styles.row}
renderItem={renderItem}
ListEmptyComponent={renderEmpty}
ListHeaderComponent={
<>
<SearchHeader />
<SearchSection />
<View style={styles.titleRow}>
<Text style={styles.sectionTitle}>{t("library.title")}</Text>
<Text style={styles.countText}>
{plants.length} {t("library.plants")}
</Text>
</View>
</>
}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/>
</SafeAreaView>
);
}
// ─── Styles ──────────────────────────────────────────────
const styles = StyleSheet.create({
safe: {
flex: 1,
backgroundColor: "#F8F9FB",
},
listContent: {
paddingBottom: 40,
},
titleRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 20,
marginBottom: 16,
},
sectionTitle: {
fontSize: 18,
fontWeight: "600",
color: "#1A1A1A",
letterSpacing: -0.4,
},
countText: {
fontSize: 13,
fontWeight: "400",
color: "#8E8E93",
},
row: {
paddingHorizontal: 20,
gap: 16,
marginBottom: 16,
},
// Card
card: {
width: CARD_WIDTH,
backgroundColor: "#FFFFFF",
borderRadius: 24,
overflow: "hidden",
borderWidth: 1,
borderColor: "#F0F0F0",
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.04,
shadowRadius: 10,
},
android: { elevation: 2 },
}),
},
imagePlaceholder: {
width: "100%",
aspectRatio: 1,
alignItems: "center",
justifyContent: "center",
position: "relative",
},
heartBtn: {
position: "absolute",
top: 10,
right: 10,
width: 34,
height: 34,
borderRadius: 12,
backgroundColor: "#FFFFFF",
alignItems: "center",
justifyContent: "center",
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.08,
shadowRadius: 4,
},
android: { elevation: 3 },
}),
},
cardInfo: {
padding: 14,
},
plantName: {
fontSize: 14,
fontWeight: "500",
color: "#1A1A1A",
marginBottom: 2,
},
plantDate: {
fontSize: 12,
fontWeight: "400",
color: "#8E8E93",
},
// Empty state
emptyContainer: {
alignItems: "center",
justifyContent: "center",
paddingVertical: 80,
paddingHorizontal: 40,
},
emptyIcon: {
width: 96,
height: 96,
borderRadius: 32,
backgroundColor: "#F0F0F0",
alignItems: "center",
justifyContent: "center",
marginBottom: 24,
},
emptyTitle: {
fontSize: 18,
fontWeight: "600",
color: "#1A1A1A",
marginBottom: 8,
},
emptyBody: {
fontSize: 14,
fontWeight: "400",
color: "#8E8E93",
textAlign: "center",
lineHeight: 20,
},
});

View file

@ -0,0 +1,424 @@
import { useState, useCallback } from "react";
import {
View,
FlatList,
TouchableOpacity,
StyleSheet,
Platform,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
// ─── Types ───────────────────────────────────────────────
type NotificationType = "health_alert" | "tip" | "system";
interface Notification {
id: string;
type: NotificationType;
title: string;
body: string;
timestamp: string;
read: boolean;
}
// ─── Mock Data ───────────────────────────────────────────
const MOCK_NOTIFICATIONS: Notification[] = [
{
id: "1",
type: "health_alert",
title: "notifications.mock.mildewAlert.title",
body: "notifications.mock.mildewAlert.body",
timestamp: "2026-04-02T14:30:00Z",
read: false,
},
{
id: "2",
type: "tip",
title: "notifications.mock.sulfurTip.title",
body: "notifications.mock.sulfurTip.body",
timestamp: "2026-04-01T09:15:00Z",
read: false,
},
{
id: "3",
type: "system",
title: "notifications.mock.scanReminder.title",
body: "notifications.mock.scanReminder.body",
timestamp: "2026-03-31T18:00:00Z",
read: true,
},
{
id: "4",
type: "health_alert",
title: "notifications.mock.botrytisAlert.title",
body: "notifications.mock.botrytisAlert.body",
timestamp: "2026-03-30T11:45:00Z",
read: true,
},
{
id: "5",
type: "tip",
title: "notifications.mock.pruningTip.title",
body: "notifications.mock.pruningTip.body",
timestamp: "2026-03-29T07:00:00Z",
read: true,
},
{
id: "6",
type: "system",
title: "notifications.mock.updateAvailable.title",
body: "notifications.mock.updateAvailable.body",
timestamp: "2026-03-28T15:30:00Z",
read: true,
},
];
// ─── Style tokens per type ───────────────────────────────
const TYPE_STYLES: Record<
NotificationType,
{ icon: string; iconColor: string; bgColor: string; dotColor: string }
> = {
health_alert: {
icon: "alert-circle",
iconColor: "#DC2626",
bgColor: "#FEE2E2",
dotColor: "#EF4444",
},
tip: {
icon: "bulb",
iconColor: "#0D9488",
bgColor: "#CCFBF1",
dotColor: "#14B8A6",
},
system: {
icon: "notifications",
iconColor: "#6366F1",
bgColor: "#E0E7FF",
dotColor: "#818CF8",
},
};
// ─── Helpers ─────────────────────────────────────────────
function timeAgo(timestamp: string, t: (key: string) => string): string {
const diff = Date.now() - new Date(timestamp).getTime();
const minutes = Math.floor(diff / 60_000);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
const days = Math.floor(hours / 24);
return `${days}d`;
}
// ─── Component ───────────────────────────────────────────
export default function NotificationsScreen() {
const { t } = useTranslation();
const navigation = useNavigation();
const [notifications, setNotifications] =
useState<Notification[]>(MOCK_NOTIFICATIONS);
const unreadCount = notifications.filter((n) => !n.read).length;
const markAllRead = useCallback(() => {
setNotifications((prev) => prev.map((n) => ({ ...n, read: true })));
}, []);
const markRead = useCallback((id: string) => {
setNotifications((prev) =>
prev.map((n) => (n.id === id ? { ...n, read: true } : n)),
);
}, []);
const renderItem = ({ item }: { item: Notification }) => {
const style = TYPE_STYLES[item.type];
return (
<TouchableOpacity
activeOpacity={0.7}
onPress={() => markRead(item.id)}
style={[styles.card, !item.read && styles.cardUnread]}
>
{/* Unread dot */}
{!item.read && (
<View style={[styles.unreadDot, { backgroundColor: style.dotColor }]} />
)}
{/* Icon */}
<View style={[styles.iconContainer, { backgroundColor: style.bgColor }]}>
<Ionicons name={style.icon as any} size={22} color={style.iconColor} />
</View>
{/* Content */}
<View style={styles.content}>
<View style={styles.titleRow}>
<Text
numberOfLines={1}
style={[styles.title, !item.read && styles.titleUnread]}
>
{t(item.title)}
</Text>
<Text style={styles.time}>{timeAgo(item.timestamp, t)}</Text>
</View>
<Text numberOfLines={2} style={styles.body}>
{t(item.body)}
</Text>
</View>
</TouchableOpacity>
);
};
const renderEmpty = () => (
<View style={styles.emptyContainer}>
<View style={styles.emptyIcon}>
<Ionicons name="notifications-off-outline" size={48} color={colors.neutral[300]} />
</View>
<Text style={styles.emptyTitle}>{t("notifications.empty.title")}</Text>
<Text style={styles.emptyBody}>{t("notifications.empty.body")}</Text>
</View>
);
return (
<SafeAreaView style={styles.safe} edges={["top"]}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backButton}
activeOpacity={0.7}
>
<Ionicons name="arrow-back" size={22} color={colors.neutral[900]} />
</TouchableOpacity>
<View style={styles.headerCenter}>
<Text style={styles.headerTitle}>
{t("common.notifications")}
</Text>
{unreadCount > 0 && (
<View style={styles.badge}>
<Text style={styles.badgeText}>{unreadCount}</Text>
</View>
)}
</View>
<TouchableOpacity
onPress={markAllRead}
style={styles.markAllButton}
activeOpacity={0.7}
disabled={unreadCount === 0}
>
<Text
style={[
styles.markAllText,
unreadCount === 0 && styles.markAllDisabled,
]}
>
{t("notifications.markAllRead")}
</Text>
</TouchableOpacity>
</View>
{/* List */}
<FlatList
data={notifications}
keyExtractor={(item) => item.id}
renderItem={renderItem}
ListEmptyComponent={renderEmpty}
contentContainerStyle={styles.list}
showsVerticalScrollIndicator={false}
ItemSeparatorComponent={() => <View style={{ height: 10 }} />}
/>
</SafeAreaView>
);
}
// ─── Styles ──────────────────────────────────────────────
const styles = StyleSheet.create({
safe: {
flex: 1,
backgroundColor: "#FAFAFA",
},
// Header
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 20,
paddingVertical: 14,
backgroundColor: "#FFFFFF",
borderBottomWidth: 1,
borderBottomColor: "#F0F0F0",
},
backButton: {
width: 40,
height: 40,
borderRadius: 14,
backgroundColor: "#F5F7F9",
alignItems: "center",
justifyContent: "center",
},
headerCenter: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
headerTitle: {
fontSize: 20,
fontWeight: "900",
color: colors.neutral[900],
letterSpacing: -0.5,
},
badge: {
backgroundColor: "#EF4444",
borderRadius: 10,
minWidth: 22,
height: 22,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 6,
},
badgeText: {
fontSize: 11,
fontWeight: "800",
color: "#FFFFFF",
},
markAllButton: {
paddingVertical: 6,
paddingHorizontal: 10,
},
markAllText: {
fontSize: 13,
fontWeight: "700",
color: colors.primary[700],
},
markAllDisabled: {
color: colors.neutral[300],
},
// List
list: {
padding: 20,
paddingBottom: 40,
},
// Card
card: {
flexDirection: "row",
alignItems: "flex-start",
backgroundColor: "#FFFFFF",
borderRadius: 24,
padding: 16,
borderWidth: 1,
borderColor: "#F0F0F0",
position: "relative",
...Platform.select({
ios: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.04,
shadowRadius: 10,
},
android: {
elevation: 2,
},
}),
},
cardUnread: {
backgroundColor: "#FAFCFF",
borderColor: "#E8EEFF",
},
// Unread dot
unreadDot: {
position: "absolute",
top: 18,
left: 10,
width: 8,
height: 8,
borderRadius: 4,
},
// Icon
iconContainer: {
width: 48,
height: 48,
borderRadius: 16,
alignItems: "center",
justifyContent: "center",
marginRight: 14,
},
// Content
content: {
flex: 1,
},
titleRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 4,
},
title: {
fontSize: 14,
fontWeight: "600",
color: colors.neutral[700],
flex: 1,
marginRight: 8,
},
titleUnread: {
fontWeight: "800",
color: colors.neutral[900],
},
time: {
fontSize: 11,
fontWeight: "600",
color: colors.neutral[400],
},
body: {
fontSize: 13,
fontWeight: "500",
color: colors.neutral[500],
lineHeight: 18,
},
// Empty state
emptyContainer: {
alignItems: "center",
justifyContent: "center",
paddingVertical: 80,
paddingHorizontal: 40,
},
emptyIcon: {
width: 96,
height: 96,
borderRadius: 32,
backgroundColor: "#F5F7F9",
alignItems: "center",
justifyContent: "center",
marginBottom: 24,
},
emptyTitle: {
fontSize: 18,
fontWeight: "800",
color: colors.neutral[900],
marginBottom: 8,
letterSpacing: -0.3,
},
emptyBody: {
fontSize: 14,
fontWeight: "500",
color: colors.neutral[400],
textAlign: "center",
lineHeight: 20,
},
});

View file

@ -1,208 +1,253 @@
import { View, Text, ScrollView, StyleSheet, TouchableOpacity, Alert } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
import { View, ScrollView, StyleSheet, Platform, Dimensions, TouchableOpacity } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import { Card } from '@/components/ui/Card';
import { XPBar } from '@/components/gamification/XPBar';
import { BadgeCard } from '@/components/gamification/BadgeCard';
import { useGameProgress } from '@/hooks/useGameProgress';
import { useHistory } from '@/hooks/useHistory';
import { colors } from '@/theme/colors';
import { typography } from '@/theme/typography';
import { spacing } from '@/theme/spacing';
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
import { useGameProgress } from "@/hooks/useGameProgress";
import type { RootStackParamList } from "@/types/navigation";
type Nav = NativeStackNavigationProp<RootStackParamList>;
const { width } = Dimensions.get("window");
const STAT_CARD_SIZE = (width - 56) / 2; // Ajusté pour le gap de 16
const BENTO_STATS = [
{ key: "scans", icon: "scan-outline", iconColor: "#F59E0B", label: "profile.totalScans" },
{ key: "grapes", icon: "leaf-outline", iconColor: "#10B981", label: "profile.uniqueGrapes" },
{ key: "streak", icon: "flame-outline", iconColor: "#EF4444", label: "profile.bestStreak" },
{ key: "xp", icon: "star-outline", iconColor: "#6366F1", label: "profile.xpTotal" },
];
export default function ProfileScreen() {
const { t } = useTranslation();
const { progress, resetProgress } = useGameProgress();
const { clearHistory } = useHistory();
const successRate =
progress.totalScans > 0
? Math.round(
(progress.totalScans -
// we don't store not_vine count separately, so approximate
0) /
progress.totalScans *
100
)
: 0;
function handleLanguageToggle() {
const newLang = i18n.language === 'fr' ? 'en' : 'fr';
i18n.changeLanguage(newLang);
}
function handleReset() {
Alert.alert(t('common.confirm'), t('profile.resetConfirm'), [
{ text: t('common.cancel'), style: 'cancel' },
{
text: t('profile.resetData'),
style: 'destructive',
onPress: async () => {
await resetProgress();
await clearHistory();
},
},
]);
const navigation = useNavigation<Nav>();
const { progress } = useGameProgress();
function handleBack() {
if (navigation.canGoBack()) {
navigation.goBack();
} else {
navigation.navigate("Main" as any);
}
}
return (
<SafeAreaView style={styles.safe} edges={['top']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Header */}
<View style={styles.header}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>🧑🌾</Text>
<View style={styles.root}>
{/* Hero Header - Style Courbé */}
<View style={styles.heroBlock}>
<SafeAreaView edges={["top"]} style={styles.heroSafeArea}>
<View style={styles.heroTopRow}>
<TouchableOpacity onPress={handleBack} style={styles.heroBackBtn}>
<Ionicons name="chevron-back" size={24} color="#FFFFFF" />
</TouchableOpacity>
<TouchableOpacity onPress={() => navigation.navigate("Settings")} style={styles.heroSettingsBtn}>
<Ionicons name="settings-outline" size={22} color={colors.primary[800]} />
</TouchableOpacity>
</View>
</SafeAreaView>
</View>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
{/* Avatar avec bague de séparation */}
<View style={styles.avatarContainer}>
<View style={styles.avatarRing}>
<View style={styles.avatar}>
<Text style={styles.avatarEmoji}>🧑🌾</Text>
</View>
</View>
<Text style={styles.username}>Vigneron</Text>
<Text style={styles.xpTotal}>{progress.xp} XP</Text>
</View>
{/* XP Bar */}
<Card style={styles.section} variant="elevated">
<XPBar xp={progress.xp} />
</Card>
{/* User Info - Focus sur la clarté */}
<View style={styles.infoCard}>
<Text style={styles.userName}>Yanis Cyrius</Text>
<Text style={styles.userEmail}>yanis@vineye.app</Text>
{/* Stats */}
<Card style={styles.section} variant="elevated">
<Text style={styles.sectionTitle}>{t('profile.stats')}</Text>
<View style={styles.statsGrid}>
<View style={styles.statItem}>
<Text style={styles.statValue}>{progress.totalScans}</Text>
<Text style={styles.statLabel}>{t('profile.totalScans')}</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{progress.uniqueGrapes.length}</Text>
<Text style={styles.statLabel}>{t('profile.uniqueGrapes')}</Text>
</View>
<View style={styles.statItem}>
<Text style={[styles.statValue, { color: colors.warning }]}>{progress.bestStreak}</Text>
<Text style={styles.statLabel}>{t('profile.bestStreak')}</Text>
</View>
<View style={styles.statItem}>
<Text style={styles.statValue}>{progress.streak}</Text>
<Text style={styles.statLabel}>{t('home.currentStreak')}</Text>
<View style={styles.actionRow}>
<TouchableOpacity style={styles.friendBtn} activeOpacity={0.8}>
<Text style={styles.friendBtnText}>+ Friends</Text>
</TouchableOpacity>
<View style={styles.xpBadge}>
<Text style={styles.xpBadgeText}>{progress.xp} XP</Text>
</View>
</View>
</Card>
</View>
{/* Badges */}
<Card style={styles.section} variant="elevated">
<Text style={styles.sectionTitle}>{t('profile.badges')}</Text>
<View style={styles.badgesGrid}>
{progress.badges.map((badge) => (
<BadgeCard key={badge.id} badge={badge} />
))}
</View>
</Card>
{/* Stats Grid - Bento Style Pur */}
<View style={styles.statsGrid}>
{BENTO_STATS.map((stat) => (
<View key={stat.key} style={styles.statCard}>
<View style={[styles.statIconWrap, { backgroundColor: `${stat.iconColor}15` }]}>
<Ionicons name={stat.icon as any} size={22} color={stat.iconColor} />
</View>
<Text style={styles.statValue}>
{stat.key === "grapes" ? (progress.uniqueGrapes?.length ?? 0) : progress[stat.key as keyof typeof progress] || 0}
</Text>
<Text style={styles.statLabel}>{t(stat.label)}</Text>
</View>
))}
</View>
{/* Settings */}
<Card style={styles.section} variant="elevated">
<TouchableOpacity style={styles.settingRow} onPress={handleLanguageToggle}>
<Text style={styles.settingLabel}>{t('profile.language')}</Text>
<Text style={styles.settingValue}>
{i18n.language === 'fr' ? '🇫🇷 Français' : '🇬🇧 English'}
</Text>
</TouchableOpacity>
<View style={styles.divider} />
<TouchableOpacity style={styles.settingRow} onPress={handleReset}>
<Text style={[styles.settingLabel, { color: colors.danger }]}>
{t('profile.resetData')}
</Text>
<Text style={styles.settingValue}></Text>
</TouchableOpacity>
</Card>
<View style={{ height: spacing['2xl'] }} />
<View style={{ height: 60 }} />
</ScrollView>
</SafeAreaView>
</View>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, backgroundColor: colors.background },
header: {
alignItems: 'center',
paddingVertical: spacing['2xl'],
gap: spacing.sm,
root: {
flex: 1,
backgroundColor: "#F8F9FB", // Gris très clair bleuté
},
heroBlock: {
height: 200,
backgroundColor: colors.primary[700],
borderBottomLeftRadius: 48,
borderBottomRightRadius: 48,
},
heroSafeArea: {
flex: 1,
},
heroTopRow: {
flexDirection: "row",
justifyContent: "space-between",
paddingHorizontal: 20,
paddingTop: 10,
},
heroBackBtn: {
width: 44,
height: 44,
borderRadius: 16,
backgroundColor: "rgba(255,255,255,0.15)",
alignItems: "center",
justifyContent: "center",
},
heroSettingsBtn: {
width: 44,
height: 44,
borderRadius: 16,
backgroundColor: "#FFFFFF",
alignItems: "center",
justifyContent: "center",
},
scrollView: {
flex: 1,
marginTop: -70,
},
scrollContent: {
paddingHorizontal: 20,
},
avatarContainer: {
alignItems: "center",
marginBottom: 20,
},
avatarRing: {
width: 110,
height: 110,
borderRadius: 55,
backgroundColor: "#FFFFFF",
alignItems: "center",
justifyContent: "center",
...Platform.select({
ios: { shadowColor: "#000", shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.1, shadowRadius: 12 },
android: { elevation: 8 },
}),
},
avatar: {
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: colors.primary[200],
alignItems: 'center',
justifyContent: 'center',
width: 96,
height: 96,
borderRadius: 48,
backgroundColor: colors.primary[50],
alignItems: "center",
justifyContent: "center",
},
avatarText: { fontSize: 40 },
username: {
fontSize: typography.fontSizes.xl,
fontWeight: typography.fontWeights.bold,
color: colors.neutral[900],
avatarEmoji: {
fontSize: 48,
},
xpTotal: {
fontSize: typography.fontSizes.sm,
color: colors.primary[700],
fontWeight: typography.fontWeights.semibold,
infoCard: {
backgroundColor: "#FFFFFF",
borderRadius: 32,
padding: 24,
alignItems: "center",
marginBottom: 20,
borderWidth: 1,
borderColor: "#F0F0F0",
},
section: {
marginHorizontal: spacing.base,
marginBottom: spacing.md,
gap: spacing.md,
userName: {
fontSize: 24,
fontWeight: "800",
color: "#1A1A1A",
letterSpacing: -0.5,
},
sectionTitle: {
fontSize: typography.fontSizes.md,
fontWeight: typography.fontWeights.semibold,
color: colors.neutral[800],
marginBottom: spacing.xs,
userEmail: {
fontSize: 14,
color: "#A0A0A0",
marginTop: 2,
marginBottom: 20,
},
actionRow: {
flexDirection: "row",
gap: 12,
},
friendBtn: {
backgroundColor: "#FFFFFF",
borderWidth: 1.5,
borderColor: "#F97316",
borderRadius: 100,
paddingHorizontal: 20,
paddingVertical: 10,
},
friendBtnText: {
fontSize: 14,
fontWeight: "600",
color: "#F97316",
},
xpBadge: {
backgroundColor: colors.primary[600],
borderRadius: 100,
paddingHorizontal: 20,
paddingVertical: 10,
justifyContent: "center",
},
xpBadgeText: {
fontSize: 14,
fontWeight: "600",
color: "#FFFFFF",
},
statsGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.sm,
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between",
},
statItem: {
width: '47%',
alignItems: 'center',
backgroundColor: colors.neutral[100],
borderRadius: 12,
paddingVertical: spacing.md,
gap: spacing.xs,
statCard: {
width: STAT_CARD_SIZE,
backgroundColor: "#FFFFFF",
borderRadius: 28,
padding: 20,
marginBottom: 16,
borderWidth: 1,
borderColor: "#F2F2F2",
},
statIconWrap: {
width: 44,
height: 44,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
marginBottom: 16,
},
statValue: {
fontSize: typography.fontSizes.xl,
fontWeight: typography.fontWeights.bold,
color: colors.primary[800],
fontSize: 22,
fontWeight: "500", // Medium au lieu de Bold pour le look premium
color: "#1A1A1A",
},
statLabel: {
fontSize: typography.fontSizes.xs,
color: colors.neutral[600],
textAlign: 'center',
},
badgesGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.sm,
},
settingRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: spacing.sm,
},
settingLabel: {
fontSize: typography.fontSizes.base,
color: colors.neutral[800],
},
settingValue: {
fontSize: typography.fontSizes.sm,
color: colors.neutral[600],
},
divider: {
height: 1,
backgroundColor: colors.neutral[200],
fontSize: 13,
color: "#9A9A9A",
marginTop: 4,
},
});

View file

@ -0,0 +1,320 @@
import {
View,
ScrollView,
StyleSheet,
Platform,
Alert,
TouchableOpacity,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import i18n from "@/i18n";
import { Text } from "@/components/ui/text";
import { colors } from "@/theme/colors";
import { useGameProgress } from "@/hooks/useGameProgress";
import { useHistory } from "@/hooks/useHistory";
interface MenuItem {
icon: string;
label: string;
rightText?: string;
rightColor?: string;
danger?: boolean;
onPress?: () => void;
}
export default function SettingsScreen() {
const { t } = useTranslation();
const navigation = useNavigation();
const { resetProgress } = useGameProgress();
const { clearHistory } = useHistory();
function handleLanguageToggle() {
const newLang = i18n.language === "fr" ? "en" : "fr";
i18n.changeLanguage(newLang);
}
function handleReset() {
Alert.alert(t("common.confirm"), t("profile.resetConfirm"), [
{ text: t("common.cancel"), style: "cancel" },
{
text: t("profile.resetData"),
style: "destructive",
onPress: async () => {
await resetProgress();
await clearHistory();
},
},
]);
}
const generalItems: MenuItem[] = [
{
icon: "person-outline",
label: t("settings.editProfile"),
},
{
icon: "globe-outline",
label: t("profile.language"),
rightText: i18n.language === "fr" ? "Français" : "English",
onPress: handleLanguageToggle,
},
{
icon: "notifications-outline",
label: t("common.notifications"),
},
{
icon: "shield-outline",
label: t("settings.privacy"),
},
];
const appItems: MenuItem[] = [
{
icon: "diamond-outline",
label: t("settings.premiumStatus"),
rightText: t("settings.inactive"),
rightColor: "#F97316",
},
{
icon: "color-palette-outline",
label: t("settings.appearance"),
},
{
icon: "help-circle-outline",
label: t("settings.helpCenter"),
},
{
icon: "document-text-outline",
label: t("settings.terms"),
},
];
const dangerItems: MenuItem[] = [
{
icon: "trash-outline",
label: t("profile.resetData"),
danger: true,
onPress: handleReset,
},
];
const renderMenuGroup = (items: MenuItem[]) => (
<View style={styles.menuCard}>
{items.map((item, index) => (
<View key={item.label}>
{index > 0 && <View style={styles.divider} />}
<TouchableOpacity
style={styles.menuRow}
activeOpacity={0.5}
onPress={item.onPress}
>
<View
style={[
styles.iconBox,
{ backgroundColor: item.danger ? "#FEF2F2" : "#F8F9FA" },
]}
>
<Ionicons
name={item.icon as any}
size={20}
color={item.danger ? "#EF4444" : "#636E72"}
/>
</View>
<Text
style={[styles.menuLabel, item.danger && styles.menuLabelDanger]}
>
{item.label}
</Text>
<View style={styles.menuRight}>
{item.rightText && (
<Text
style={[
styles.menuRightText,
item.rightColor && { color: item.rightColor },
]}
>
{item.rightText}
</Text>
)}
<Ionicons name="chevron-forward" size={14} color="#D1D1D6" />
</View>
</TouchableOpacity>
</View>
))}
</View>
);
return (
<SafeAreaView style={styles.safe} edges={["top"]}>
{/* Header épuré style Bumble/Apple */}
<View style={styles.header}>
<TouchableOpacity
onPress={() => navigation.goBack()}
style={styles.backBtn}
>
<Ionicons name="chevron-back" size={24} color="#1A1A1A" />
</TouchableOpacity>
<Text style={styles.headerTitle}>{t("common.settings")}</Text>
<View style={{ width: 44 }} />
</View>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
<Text style={styles.sectionLabel}>{t("settings.general")}</Text>
{renderMenuGroup(generalItems)}
<Text style={styles.sectionLabel}>{t("settings.app")}</Text>
{renderMenuGroup(appItems)}
{/* Banner Referral plus "Flat" et moderne */}
<TouchableOpacity style={styles.referCard} activeOpacity={0.9}>
<View style={styles.referContent}>
<Text style={styles.referTitle}>Refer a friend</Text>
<Text style={styles.referBody}>
Get $50 per successful referral
</Text>
</View>
<View style={styles.referIconWrap}>
<Ionicons name="gift" size={28} color="#FFFFFF" />
</View>
</TouchableOpacity>
{renderMenuGroup(dangerItems)}
<Text style={styles.versionText}>VinEye Version 1.0.0</Text>
<View style={{ height: 40 }} />
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: {
flex: 1,
backgroundColor: "#F8F9FB", // Gris encore plus clair/bleuté
},
header: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
paddingVertical: 10,
backgroundColor: "transparent", // Pas de démarcation brutale
},
backBtn: {
width: 44,
height: 44,
alignItems: "center",
justifyContent: "center",
borderRadius: 14,
backgroundColor: "#FFFFFF",
borderWidth: 1,
borderColor: "#F0F0F0",
},
headerTitle: {
fontSize: 18,
fontWeight: "600", // Pas de Bold 900 ici, juste Medium/SemiBold
color: "#1A1A1A",
letterSpacing: -0.4,
},
scrollContent: {
paddingHorizontal: 20,
paddingTop: 10,
},
sectionLabel: {
fontSize: 12,
fontWeight: "500",
color: "#A0A0A0",
marginBottom: 12,
marginLeft: 4,
textTransform: "uppercase",
letterSpacing: 1,
},
menuCard: {
backgroundColor: "#FFFFFF",
borderRadius: 24,
marginBottom: 20,
borderWidth: 1,
borderColor: "#F2F2F2",
},
menuRow: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 16,
},
iconBox: {
width: 36,
height: 36,
borderRadius: 10,
alignItems: "center",
justifyContent: "center",
marginRight: 12,
},
menuLabel: {
flex: 1,
fontSize: 15,
fontWeight: "400", // On reste sur du Regular
color: "#2D3436",
},
menuLabelDanger: {
color: "#EF4444",
},
menuRight: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
menuRightText: {
fontSize: 14,
color: "#B2B2B2",
},
divider: {
height: 1,
backgroundColor: "#F8F9FA",
marginLeft: 60, // Aligné avec le texte, pas l'icône
},
referCard: {
backgroundColor: "#F97316",
borderRadius: 24,
padding: 20,
marginBottom: 20,
flexDirection: "row",
alignItems: "center",
},
referContent: {
flex: 1,
},
referTitle: {
fontSize: 17,
fontWeight: "700",
color: "#FFFFFF",
},
referBody: {
fontSize: 13,
color: "rgba(255,255,255,0.7)",
marginTop: 2,
},
referIconWrap: {
width: 50,
height: 50,
borderRadius: 15,
backgroundColor: "rgba(255,255,255,0.2)",
alignItems: "center",
justifyContent: "center",
},
versionText: {
textAlign: "center",
fontSize: 12,
color: "#D1D1D6",
marginTop: 10,
},
});

View file

@ -5,11 +5,16 @@ export type RootStackParamList = {
Main: undefined;
Result: { detection: Detection };
Notifications: undefined;
Profile: undefined;
Settings: undefined;
Guides: undefined;
Library: undefined;
};
export type BottomTabParamList = {
Home: undefined;
Guides: undefined;
Scanner: undefined;
Library: undefined;
Map: undefined;
};