Compare commits

..

No commits in common. "af299e816abc0ddb96f934e2bdd6f80fb1c1645c" and "a964cc3836b16e57303037b3b8a93c31ab66f672" have entirely different histories.

32 changed files with 599 additions and 3555 deletions

3
.gitignore vendored
View file

@ -23,6 +23,3 @@ VinEye/.expo/
VinEye/dist/
VinEye/ios/
VinEye/android/
# dependances
node_modules/

View file

@ -1,65 +0,0 @@
# Repository Guidelines
Monorepo with two components: a **Python/TensorFlow CNN** for grapevine disease detection and a **React Native (Expo) mobile app** (VinEye) that runs the model on-device via TFLite.
## Project Structure & Module Organization
```
venv/src/ # Python ML pipeline (training, evaluation, attribution)
venv/models/ # Trained model artifacts (.keras, .tflite)
docs/images/ # Dataset & results visualizations
VinEye/ # Expo React Native mobile app
src/screens/ # 6 screens: Splash, Home, Scanner, Result, History, Profile
src/components/ # UI grouped by feature (gamification/, scanner/, history/, ui/)
src/services/ # TFLite inference, AsyncStorage, haptics
src/hooks/ # useDetection, useGameProgress, useHistory
src/navigation/ # React Navigation v7 (BottomTabs + NativeStack)
src/i18n/ # FR + EN translations (i18next)
src/theme/ # Design tokens (primary #2D6A4F, accent #7C3AED)
```
The ML model currently uses a mock TFLite detector in the mobile app (weighted random: 70% vine / 20% uncertain / 10% not_vine). The CNN trains on 9027 images (256x256) across 4 classes: Black Rot, ESCA, Healthy, Leaf Blight.
## Build, Test, and Development Commands
### VinEye (Mobile)
```bash
cd VinEye
pnpm install # Install dependencies (pnpm only, never npm/yarn)
pnpm start # Start Expo dev server
pnpm android # Run on Android
pnpm ios # Run on iOS
pnpm web # Run on web
```
### Python ML Pipeline
```bash
cd venv/src
python data_split.py # Split raw data into train/val/test (80/10/10)
python data_explore.py # EDA: class distribution, sample visualization
python model_train.py # Train CNN, exports .keras + .tflite to venv/models/
python evaluate_model.py # Accuracy/loss curves, confusion matrix, top-k predictions
python gradient.py # Integrated gradients attribution masks
```
Scripts must be run from `venv/src/` — paths are derived relative to that directory.
## Coding Style & Naming Conventions
**TypeScript (VinEye):**
- Strict mode enabled, path alias `@/*` maps to `src/*`
- Max 300 lines per file
- NativeWind (TailwindCSS) for styling — no inline styles
- `useEffect` must be imported from `react`, never from `react-native-reanimated`
- React Navigation v7 only (Expo Router is forbidden)
- RN-native UI components only (no web-based component libraries)
**Python:** TensorFlow/Keras Sequential API, scripts use `from module import *` pattern.
No linter or formatter configs are enforced.
## Commit Guidelines
Commit messages are informal, descriptive, lowercase. No conventional commits format is enforced. Examples from history: `add VinEye frontend app + fix hardcoded paths + gitignore`, `update`, `maj`.

View file

@ -1,7 +1,7 @@
# VinEye
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.
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.
---
@ -10,16 +10,17 @@ Cible des amateurs de vin/jardinage. Scan par camera, identification de maladies
| Couche | Technologies |
|--------|-------------|
| Framework | React Native + Expo SDK 54 (bare workflow) |
| Navigation | React Navigation v7 (NativeStack + BottomTabs) |
| Navigation | React Navigation v7 (NativeStack + BottomTabs) **PAS Expo Router** |
| Langage | TypeScript strict |
| 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 |
| 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`) |
| i18n | i18next + react-i18next (FR + EN) |
| Camera | expo-camera |
| Caméra | expo-camera |
| Haptics | expo-haptics |
| SVG | react-native-svg |
| Lottie | lottie-react-native |
| Package manager | **pnpm** |
---
@ -28,25 +29,29 @@ Cible des amateurs de vin/jardinage. Scan par camera, identification de maladies
```
VinEye/
├── App.tsx
├── App.tsx # Entry point (i18n init + RootNavigator)
├── src/
│ ├── assets/
│ │ ├── images/ # logo.svg, icon.png, splash.png
│ │ └── lottie/ # confetti.json, scan-success.json, vine-leaf.json, level-up.json
│ ├── components/
│ │ ├── ui/ # Text, Button, Card, Badge, ProgressCircle
│ │ ├── home/ # SearchHeader, SearchSection, HomeCta, FrequentDiseases,
│ │ │ # SeasonAlert, PracticalGuides, statssection, gamificationstat
│ │ │ └── components/ # homeheader (SectionHeader)
│ │ ├── ui/ # Button, Card, Badge, ProgressCircle, AnimatedCounter
│ │ ├── scanner/ # DetectionFrame, CameraOverlay, ConfidenceMeter
│ │ ├── gamification/ # XPBar, BadgeCard, ProgressRing, LevelIndicator
│ │ ├── gamification/ # XPBar, BadgeCard, LevelIndicator, StreakCounter
│ │ └── 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/ # 11 ecrans (voir Navigation)
│ ├── services/ # tflite/model.ts, storage.ts, haptics.ts
│ ├── theme/ # colors.ts, typography.ts, spacing.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
│ ├── types/ # detection.ts, gamification.ts, navigation.ts
│ └── utils/ # cepages.ts, achievements.ts
│ └── utils/
│ ├── cepages.ts # 15 cépages (origine, couleur, caractéristiques, régions)
│ └── achievements.ts # XP_REWARDS, LEVELS, BADGE_DEFINITIONS, checkNewBadges, getLevelForXP
```
---
@ -54,137 +59,71 @@ VinEye/
## Navigation
```
RootNavigator (NativeStack)
├── Splash → SplashScreen (auto → Main apres 2.8s)
RootNavigator (Stack)
├── Splash → SplashScreen (auto-navigate vers Main après 2.8s)
├── Main → BottomTabNavigator
│ ├── Home → HomeScreen
│ ├── 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)
│ ├── Scanner → ScannerScreen (bouton FAB central)
│ ├── History → HistoryScreen
│ └── Profile → ProfileScreen
└── Result (modal) → ResultScreen (slide_from_bottom)
```
**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
---
## 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 |
---
## Ecrans
## Gamification
| 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 |
- **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
---
## Composants Home
## Fonctionnalités clés
| 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
| 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/` | ✅ |
---
## Conventions
- **Styling** : NativeWind (className) prioritaire, StyleSheet pour ombres/gradients/arrondis specifiques
- Package manager : **pnpm**
- Path alias : `@/*``src/*`
- `useEffect` depuis `react` (jamais depuis reanimated)
- Navigation : React Navigation v7, **jamais Expo Router**
- `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/`)
- Max 300 lignes par fichier
- i18n : tous les textes via `t()`, cles dans fr.json et en.json
---
## Commandes
```bash
pnpm start # Metro bundler
pnpm web # Version web
pnpm start # Lance Metro bundler
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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -28,10 +28,8 @@
"expo-status-bar": "~3.0.9",
"i18next": "^26.0.1",
"lottie-react-native": "^7.3.6",
"lucide-react-native": "^1.7.0",
"nativewind": "^4.2.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-i18next": "^17.0.1",
"react-lucid": "^0.0.1",
"react-native": "0.81.5",
@ -39,7 +37,6 @@
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "^15.12.1",
"react-native-web": "^0.21.2",
"react-native-worklets": "0.5.1",
"tailwind-merge": "^3.5.0",
"tailwindcss": "3.4.17"

View file

@ -25,13 +25,13 @@ importers:
version: 7.14.10(@react-navigation/native@7.2.2(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@rn-primitives/portal':
specifier: ^1.4.0
version: 1.4.0(@types/react@19.1.17)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0))
version: 1.4.0(@types/react@19.1.17)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0))
'@rn-primitives/separator':
specifier: ^1.4.0
version: 1.4.0(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
version: 1.4.0(@types/react@19.1.17)(react-dom@19.2.4(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@rn-primitives/slot':
specifier: ^1.4.0
version: 1.4.0(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
version: 1.4.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
class-variance-authority:
specifier: ^0.7.1
version: 0.7.1
@ -43,13 +43,13 @@ importers:
version: 54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-camera:
specifier: ~17.0.10
version: 17.0.10(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
version: 17.0.10(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-haptics:
specifier: ~15.0.8
version: 15.0.8(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))
expo-image:
specifier: ~3.0.11
version: 3.0.11(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
version: 3.0.11(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-linear-gradient:
specifier: ~15.0.8
version: 15.0.8(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@ -65,21 +65,15 @@ importers:
lottie-react-native:
specifier: ^7.3.6
version: 7.3.6(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
lucide-react-native:
specifier: ^1.7.0
version: 1.7.0(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
nativewind:
specifier: ^4.2.3
version: 4.2.3(react-native-reanimated@4.1.7(react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tailwindcss@3.4.17)
react:
specifier: 19.1.0
version: 19.1.0
react-dom:
specifier: 19.1.0
version: 19.1.0(react@19.1.0)
react-i18next:
specifier: ^17.0.1
version: 17.0.1(i18next@26.0.1(typescript@5.9.3))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3)
version: 17.0.1(i18next@26.0.1(typescript@5.9.3))(react-dom@19.2.4(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3)
react-lucid:
specifier: ^0.0.1
version: 0.0.1
@ -98,9 +92,6 @@ importers:
react-native-svg:
specifier: ^15.12.1
version: 15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react-native-web:
specifier: ^0.21.2
version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react-native-worklets:
specifier: 0.5.1
version: 0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
@ -941,9 +932,6 @@ packages:
resolution: {integrity: sha512-KlRawK4aXxRLlR3HYVfZKhfQp7sejQefQ/LttUWUkErhKO0AFt+yznoSLq7xwIrH9K3A3YwImHuFVtUtuDmurA==}
engines: {node: '>= 20.19.4'}
'@react-native/normalize-colors@0.74.89':
resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==}
'@react-native/normalize-colors@0.81.5':
resolution: {integrity: sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==}
@ -1455,16 +1443,10 @@ packages:
core-js-compat@3.49.0:
resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==}
cross-fetch@3.2.0:
resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==}
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
css-in-js-utils@3.1.0:
resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==}
css-select@5.2.2:
resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==}
@ -1760,12 +1742,6 @@ packages:
fb-watchman@2.0.2:
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
fbjs-css-vars@1.0.2:
resolution: {integrity: sha512-b2XGFAFdWZWg0phtAWLHCk836A1Xann+I+Dgd3Gk64MHKZO44FfoD1KxyvbSh0qZsIoXQGGlVztIY+oitJPpRQ==}
fbjs@3.0.5:
resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==}
fdir@6.5.0:
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
engines: {node: '>=12.0.0'}
@ -1896,9 +1872,6 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
hyphenate-style-name@1.1.0:
resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==}
i18next@26.0.1:
resolution: {integrity: sha512-vtz5sXU4+nkCm8yEU+JJ6yYIx0mkg9e68W0G0PXpnOsmzLajNsW5o28DJMqbajxfsfq0gV3XdrBudsDQnwxfsQ==}
peerDependencies:
@ -1933,9 +1906,6 @@ packages:
ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
inline-style-prefixer@7.0.1:
resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==}
invariant@2.2.4:
resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
@ -2254,13 +2224,6 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
lucide-react-native@1.7.0:
resolution: {integrity: sha512-wGJY5nosSawh028jg8r1ZKqnGPDIVfIL9xvKOs4wPYFQHeJMHsADYm/lmuFYXMXXatSkHhpsCjeqIRgeFGzf8g==}
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-native: '*'
react-native-svg: ^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0
makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
@ -2273,9 +2236,6 @@ packages:
memoize-one@5.2.1:
resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==}
memoize-one@6.0.0:
resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
merge-options@3.0.4:
resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==}
engines: {node: '>=10'}
@ -2494,15 +2454,6 @@ packages:
nested-error-stacks@2.0.1:
resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==}
node-fetch@2.7.0:
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
engines: {node: 4.x || >=6.0.0}
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
node-forge@1.4.0:
resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==}
engines: {node: '>= 6.13.0'}
@ -2705,9 +2656,6 @@ packages:
resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
engines: {node: '>=0.4.0'}
promise@7.3.1:
resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==}
promise@8.3.0:
resolution: {integrity: sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==}
@ -2744,10 +2692,10 @@ packages:
react-devtools-core@6.1.5:
resolution: {integrity: sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==}
react-dom@19.1.0:
resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
react-dom@19.2.4:
resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==}
peerDependencies:
react: ^19.1.0
react: ^19.2.4
react-freeze@1.0.4:
resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==}
@ -2827,12 +2775,6 @@ packages:
react: '*'
react-native: '*'
react-native-web@0.21.2:
resolution: {integrity: sha512-SO2t9/17zM4iEnFvlu2DA9jqNbzNhoUP+AItkoCOyFmDMOhUnBBznBDCYN92fGdfAkfQlWzPoez6+zLxFNsZEg==}
peerDependencies:
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
react-native-worklets@0.5.1:
resolution: {integrity: sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==}
peerDependencies:
@ -2947,6 +2889,9 @@ packages:
scheduler@0.26.0:
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
semver@6.3.1:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true
@ -2973,9 +2918,6 @@ packages:
resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==}
engines: {node: '>= 0.8.0'}
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
@ -3083,9 +3025,6 @@ packages:
structured-headers@0.4.1:
resolution: {integrity: sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==}
styleq@0.1.3:
resolution: {integrity: sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==}
sucrase@3.35.1:
resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==}
engines: {node: '>=16 || 14 >=14.17'}
@ -3166,9 +3105,6 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
tr46@0.0.3:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
@ -3189,10 +3125,6 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
ua-parser-js@1.0.41:
resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==}
hasBin: true
undici-types@7.18.2:
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
@ -3271,9 +3203,6 @@ packages:
wcwidth@1.0.1:
resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==}
webidl-conversions@3.0.1:
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
webidl-conversions@5.0.0:
resolution: {integrity: sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==}
engines: {node: '>=8'}
@ -3285,9 +3214,6 @@ packages:
resolution: {integrity: sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==}
engines: {node: '>=10'}
whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
@ -4413,19 +4339,19 @@ snapshots:
optionalDependencies:
'@types/react': 19.1.17
'@radix-ui/react-primitive@2.1.4(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@radix-ui/react-primitive@2.1.4(@types/react@19.1.17)(react-dom@19.2.4(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-slot': 1.2.4(@types/react@19.1.17)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-dom: 19.2.4(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.17
'@radix-ui/react-separator@1.1.8(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
'@radix-ui/react-separator@1.1.8(@types/react@19.1.17)(react-dom@19.2.4(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.4(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.4(@types/react@19.1.17)(react-dom@19.2.4(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
react-dom: 19.2.4(react@19.1.0)
optionalDependencies:
'@types/react': 19.1.17
@ -4634,8 +4560,6 @@ snapshots:
- supports-color
optional: true
'@react-native/normalize-colors@0.74.89': {}
'@react-native/normalize-colors@0.81.5': {}
'@react-native/virtualized-lists@0.81.5(@types/react@19.1.17)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
@ -4710,45 +4634,41 @@ snapshots:
dependencies:
nanoid: 3.3.11
'@rn-primitives/portal@1.4.0(@types/react@19.1.17)(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0))':
'@rn-primitives/portal@1.4.0(@types/react@19.1.17)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0))':
dependencies:
react: 19.1.0
zustand: 5.0.12(@types/react@19.1.17)(react@19.1.0)(use-sync-external-store@1.6.0(react@19.1.0))
optionalDependencies:
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
transitivePeerDependencies:
- '@types/react'
- immer
- use-sync-external-store
'@rn-primitives/separator@1.4.0(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
'@rn-primitives/separator@1.4.0(@types/react@19.1.17)(react-dom@19.2.4(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-separator': 1.1.8(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@rn-primitives/slot': 1.4.0(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@rn-primitives/types': 1.4.0(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@radix-ui/react-separator': 1.1.8(@types/react@19.1.17)(react-dom@19.2.4(react@19.1.0))(react@19.1.0)
'@rn-primitives/slot': 1.4.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
'@rn-primitives/types': 1.4.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react: 19.1.0
optionalDependencies:
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
transitivePeerDependencies:
- '@types/react'
- '@types/react-dom'
- react-dom
'@rn-primitives/slot@1.4.0(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
'@rn-primitives/slot@1.4.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@rn-primitives/types@1.4.0(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
'@rn-primitives/types@1.4.0(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@sinclair/typebox@0.27.10': {}
@ -5231,22 +5151,12 @@ snapshots:
dependencies:
browserslist: 4.28.1
cross-fetch@3.2.0:
dependencies:
node-fetch: 2.7.0
transitivePeerDependencies:
- encoding
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
css-in-js-utils@3.1.0:
dependencies:
hyphenate-style-name: 1.1.0
css-select@5.2.2:
dependencies:
boolbase: 1.0.0
@ -5370,14 +5280,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
expo-camera@17.0.10(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
expo-camera@17.0.10(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
invariant: 2.2.4
react: 19.1.0
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
optionalDependencies:
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
expo-constants@18.0.13(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)):
dependencies:
@ -5404,13 +5312,11 @@ snapshots:
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo-image@3.0.11(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
expo-image@3.0.11(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
optionalDependencies:
react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
expo-keep-awake@15.0.8(expo@54.0.33(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react@19.1.0):
dependencies:
@ -5506,20 +5412,6 @@ snapshots:
dependencies:
bser: 2.1.1
fbjs-css-vars@1.0.2: {}
fbjs@3.0.5:
dependencies:
cross-fetch: 3.2.0
fbjs-css-vars: 1.0.2
loose-envify: 1.4.0
object-assign: 4.1.1
promise: 7.3.1
setimmediate: 1.0.5
ua-parser-js: 1.0.41
transitivePeerDependencies:
- encoding
fdir@6.5.0(picomatch@4.0.4):
optionalDependencies:
picomatch: 4.0.4
@ -5644,8 +5536,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
hyphenate-style-name@1.1.0: {}
i18next@26.0.1(typescript@5.9.3):
dependencies:
'@babel/runtime': 7.29.2
@ -5671,10 +5561,6 @@ snapshots:
ini@1.3.8: {}
inline-style-prefixer@7.0.1:
dependencies:
css-in-js-utils: 3.1.0
invariant@2.2.4:
dependencies:
loose-envify: 1.4.0
@ -5954,12 +5840,6 @@ snapshots:
dependencies:
yallist: 3.1.1
lucide-react-native@1.7.0(react-native-svg@15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
react: 19.1.0
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
react-native-svg: 15.12.1(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
makeerror@1.0.12:
dependencies:
tmpl: 1.0.5
@ -5970,8 +5850,6 @@ snapshots:
memoize-one@5.2.1: {}
memoize-one@6.0.0: {}
merge-options@3.0.4:
dependencies:
is-plain-obj: 2.1.0
@ -6406,10 +6284,6 @@ snapshots:
nested-error-stacks@2.0.1: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
node-forge@1.4.0: {}
node-int64@0.4.0: {}
@ -6583,10 +6457,6 @@ snapshots:
progress@2.0.3: {}
promise@7.3.1:
dependencies:
asap: 2.0.6
promise@8.3.0:
dependencies:
asap: 2.0.6
@ -6630,16 +6500,16 @@ snapshots:
- bufferutil
- utf-8-validate
react-dom@19.1.0(react@19.1.0):
react-dom@19.2.4(react@19.1.0):
dependencies:
react: 19.1.0
scheduler: 0.26.0
scheduler: 0.27.0
react-freeze@1.0.4(react@19.1.0):
dependencies:
react: 19.1.0
react-i18next@17.0.1(i18next@26.0.1(typescript@5.9.3))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3):
react-i18next@17.0.1(i18next@26.0.1(typescript@5.9.3))(react-dom@19.2.4(react@19.1.0))(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(typescript@5.9.3):
dependencies:
'@babel/runtime': 7.29.2
html-parse-stringify: 3.0.1
@ -6647,7 +6517,7 @@ snapshots:
react: 19.1.0
use-sync-external-store: 1.6.0(react@19.1.0)
optionalDependencies:
react-dom: 19.1.0(react@19.1.0)
react-dom: 19.2.4(react@19.1.0)
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
typescript: 5.9.3
@ -6709,21 +6579,6 @@ snapshots:
react-native: 0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)
warn-once: 0.1.1
react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0):
dependencies:
'@babel/runtime': 7.29.2
'@react-native/normalize-colors': 0.74.89
fbjs: 3.0.5
inline-style-prefixer: 7.0.1
memoize-one: 6.0.0
nullthrows: 1.1.1
postcss-value-parser: 4.2.0
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
styleq: 0.1.3
transitivePeerDependencies:
- encoding
react-native-worklets@0.5.1(@babel/core@7.29.0)(react-native@0.81.5(@babel/core@7.29.0)(@react-native/metro-config@0.84.1(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
'@babel/core': 7.29.0
@ -6874,6 +6729,8 @@ snapshots:
scheduler@0.26.0: {}
scheduler@0.27.0: {}
semver@6.3.1: {}
semver@7.7.2: {}
@ -6909,8 +6766,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
setimmediate@1.0.5: {}
setprototypeof@1.2.0: {}
sf-symbols-typescript@2.2.0: {}
@ -6992,8 +6847,6 @@ snapshots:
structured-headers@0.4.1: {}
styleq@0.1.3: {}
sucrase@3.35.1:
dependencies:
'@jridgewell/gen-mapping': 0.3.13
@ -7105,8 +6958,6 @@ snapshots:
toidentifier@1.0.1: {}
tr46@0.0.3: {}
ts-interface-checker@0.1.13: {}
type-detect@4.0.8: {}
@ -7117,8 +6968,6 @@ snapshots:
typescript@5.9.3: {}
ua-parser-js@1.0.41: {}
undici-types@7.18.2: {}
undici@6.24.1: {}
@ -7174,8 +7023,6 @@ snapshots:
dependencies:
defaults: 1.0.4
webidl-conversions@3.0.1: {}
webidl-conversions@5.0.0: {}
whatwg-fetch@3.6.20: {}
@ -7186,11 +7033,6 @@ snapshots:
punycode: 2.3.1
webidl-conversions: 5.0.0
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
which@2.0.2:
dependencies:
isexe: 2.0.0

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -1,149 +0,0 @@
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

@ -1,191 +0,0 @@
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

@ -1,112 +0,0 @@
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,117 +0,0 @@
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";
type Nav = NativeStackNavigationProp<RootStackParamList>;
export default function SearchHeader() {
const { t } = useTranslation();
const navigation = useNavigation<Nav>();
return (
<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
style={styles.notifButton}
activeOpacity={0.7}
onPress={() => navigation.navigate("Notifications")}
>
<Ionicons
name="notifications-outline"
size={22}
color={colors.neutral[800]}
/>
<View style={styles.notifBadge} />
</TouchableOpacity>
<TouchableOpacity
style={styles.notifButton}
activeOpacity={0.7}
onPress={() => navigation.navigate("Profile")}
>
<Ionicons
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

@ -1,79 +0,0 @@
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

@ -1,104 +0,0 @@
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,9 +1,32 @@
import { View, TouchableOpacity, StyleSheet } from "react-native";
import { useEffect } from "react";
import { View, ScrollView, TouchableOpacity, TextInput } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native";
import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
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 { BottomTabParamList } from "@/types/navigation";
import StatCard from "@/components/home/gamificationstat";
import StatisticsSection from "@/components/home/statssection";
export default function SectionHeader({
title,
@ -13,76 +36,21 @@ export default function SectionHeader({
onViewAll?: () => void;
}) {
const { t } = useTranslation();
return (
<View style={styles.container}>
{/* Titre avec graisse plus affirmée */}
<Text style={styles.title}>
<View className="flex-row items-center justify-between">
<Text className="text-[17px] font-semibold text-neutral-900">
{title}
</Text>
{onViewAll && (
<TouchableOpacity
onPress={onViewAll}
activeOpacity={0.6}
style={styles.button}
<TouchableOpacity onPress={onViewAll}>
<Text
className="text-[13px] font-medium"
style={{ color: colors.primary[700] }}
>
<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,
},
});

View file

@ -1,129 +0,0 @@
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",
},
];

View file

@ -1,35 +0,0 @@
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

@ -9,16 +9,11 @@
"confirm": "Confirm",
"loading": "Loading...",
"error": "Error",
"retry": "Retry",
"map": "Map",
"notifications": "Notifications",
"settings": "Settings",
"details": "Details"
"retry": "Retry"
},
"home": {
"greeting": "Hello, Winemaker!",
"scanButton": "Scan a vine",
"searchPlaceholder": "Search a disease, a grape variety...",
"totalScans": "Total scans",
"uniqueGrapes": "Grapes found",
"currentStreak": "Current streak",
@ -29,142 +24,7 @@
"bannerButton": "Get started",
"lastScan": "Last scan",
"noScansYet": "No scans yet",
"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..."
}
"startScanning": "Start scanning!"
},
"scanner": {
"scanning": "Analyzing...",
@ -219,21 +79,7 @@
"language": "Language",
"resetData": "Reset data",
"resetConfirm": "Are you sure you want to reset all data?",
"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."
"days": "days"
},
"achievements": {
"firstScan": "First Scan",

View file

@ -9,16 +9,11 @@
"confirm": "Confirmer",
"loading": "Chargement...",
"error": "Erreur",
"retry": "Réessayer",
"map": "Carte",
"notifications": "Notifications",
"settings": "Paramètres",
"details": "Détails"
"retry": "Réessayer"
},
"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",
@ -29,142 +24,7 @@
"bannerButton": "Commencer",
"lastScan": "Dernier scan",
"noScansYet": "Aucun scan pour l'instant",
"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..."
}
"startScanning": "Commencez à scanner !"
},
"scanner": {
"scanning": "Analyse en cours...",
@ -219,21 +79,7 @@
"language": "Langue",
"resetData": "Réinitialiser les données",
"resetConfirm": "Êtes-vous sûr de vouloir réinitialiser toutes les données ?",
"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é."
"days": "jours"
},
"achievements": {
"firstScan": "Premier Scan",

View file

@ -1,55 +1,62 @@
import React from "react";
import { View, Text, TouchableOpacity, Platform } from "react-native";
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, Map, BookOpen, Leaf } from "lucide-react-native";
import React from 'react';
import { View, Text, TouchableOpacity, Platform, LayoutAnimation, UIManager } from 'react-native';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { useTranslation } from 'react-i18next';
import * as Haptics from 'expo-haptics';
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";
// Imports de tes écrans
import HomeScreen from '@/screens/HomeScreen';
import ScannerScreen from '@/screens/ScannerScreen';
import HistoryScreen from '@/screens/HistoryScreen';
import ProfileScreen from '@/screens/ProfileScreen';
// Activation de LayoutAnimation pour Android
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
const Tab = createBottomTabNavigator();
const TAB_ICONS: Record<string, any> = {
Home: House,
Guides: BookOpen,
Library: Leaf,
Map: Map,
};
function MyCustomTabBar({ state, descriptors, navigation }: any) {
const insets = useSafeAreaInsets();
const { t } = useTranslation();
// Gestion de la marge basse pour éviter la superposition avec la barre système
const safeBottom = Platform.OS === 'android' ? Math.max(insets.bottom, 24) : insets.bottom;
return (
<View
className="absolute bg-white flex-row items-center justify-between px-2"
style={{
flexDirection: "row",
backgroundColor: colors.surface,
borderTopWidth: 1,
borderTopColor: colors.neutral[300],
paddingBottom: insets.bottom,
paddingTop: 8,
alignItems: "flex-end",
bottom: safeBottom + 10,
left: 20,
right: 20,
height: 70,
borderRadius: 35,
elevation: 12,
shadowColor: '#000',
shadowOffset: { width: 0, height: 10 },
shadowOpacity: 0.15,
shadowRadius: 20,
}}
>
{state.routes.map((route: any, index: number) => {
const { options } = descriptors[route.key];
const isFocused = state.index === index;
const label = options.tabBarLabel || route.name;
const isScanner = route.name === "Scanner";
const onPress = () => {
if (Platform.OS !== "web") {
// 1. Retour Haptique (Vibration légère "Impact")
if (Platform.OS !== 'web') {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
// 2. Animation de la transition (Pill expansion)
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
const event = navigation.emit({
type: "tabPress",
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
@ -59,80 +66,48 @@ function MyCustomTabBar({ state, descriptors, navigation }: any) {
}
};
// FAB central pour Scanner
if (isScanner) {
return (
<TouchableOpacity
key={route.key}
onPress={onPress}
activeOpacity={0.8}
accessibilityRole="button"
accessibilityLabel={label}
style={{
flex: 1,
alignItems: "center",
justifyContent: "center",
}}
>
<View
style={{
width: 56,
height: 56,
borderRadius: 28,
backgroundColor: colors.primary[800],
alignItems: "center",
justifyContent: "center",
marginTop: -28,
shadowColor: colors.primary[900],
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 8,
elevation: 8,
}}
>
<ScanLine size={26} color="#FFFFFF" />
</View>
</TouchableOpacity>
);
// Choix de l'icône (Outline vs Solid)
const getIcon = (name: string, focused: boolean) => {
switch (name) {
case 'Home': return focused ? 'home' : 'home-outline';
case 'History': return focused ? 'receipt' : 'receipt-outline';
case 'Scanner': return focused ? 'scan' : 'scan-outline';
case 'Profile': return focused ? 'person' : 'person-outline';
default: return 'help-outline';
}
};
// Onglets classiques (Home, Map)
const Icon = TAB_ICONS[route.name];
const tintColor = isFocused ? colors.primary[700] : colors.neutral[400];
const label = options.tabBarLabel || route.name;
return (
<TouchableOpacity
key={route.key}
key={index}
onPress={onPress}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityState={isFocused ? { selected: true } : {}}
accessibilityLabel={label}
style={{
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingVertical: 6,
}}
style={{ flex: isFocused ? 2 : 1 }}
className="items-center justify-center h-full"
>
{Icon && (
<Icon
<View
className={`flex-row items-center justify-center py-2.5 ${
isFocused ? 'bg-gray-900 px-5' : 'bg-transparent px-0'
}`}
style={{ borderRadius: 999 }}
>
<Ionicons
name={getIcon(route.name, isFocused) as any}
size={22}
color={tintColor}
strokeWidth={isFocused ? 2.5 : 1.8}
color={isFocused ? '#FFFFFF' : '#9CA3AF'}
/>
)}
{/* <Text
{isFocused && (
<Text
numberOfLines={1}
style={{
fontSize: 11,
marginTop: 4,
color: tintColor,
fontWeight: isFocused ? "600" : "400",
}}
className="ml-2 text-white font-bold text-[13px]"
>
{label}
</Text> */}
</Text>
)}
</View>
</TouchableOpacity>
);
})}
@ -151,27 +126,22 @@ export default function BottomTabNavigator() {
<Tab.Screen
name="Home"
component={HomeScreen}
options={{ tabBarLabel: t("common.home") }}
options={{ tabBarLabel: t('common.home') }}
/>
<Tab.Screen
name="Guides"
component={GuidesScreen}
options={{ tabBarLabel: t("guides.screenTitle") }}
name="History"
component={HistoryScreen}
options={{ tabBarLabel: t('common.history') }}
/>
<Tab.Screen
name="Scanner"
component={ScannerScreen}
options={{ tabBarLabel: t("common.scan") }}
options={{ tabBarLabel: 'Scan' }}
/>
<Tab.Screen
name="Library"
component={LibraryScreen}
options={{ tabBarLabel: t("library.title") }}
/>
<Tab.Screen
name="Map"
component={MapScreen}
options={{ tabBarLabel: t("common.map") }}
name="Profile"
component={ProfileScreen}
options={{ tabBarLabel: t('common.profile') }}
/>
</Tab.Navigator>
);

View file

@ -3,11 +3,6 @@ import { NavigationContainer } from '@react-navigation/native';
import SplashScreen from '@/screens/SplashScreen';
import ResultScreen from '@/screens/ResultScreen';
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';
@ -28,31 +23,6 @@ export default function RootNavigator() {
component={ResultScreen}
options={{ animation: 'slide_from_bottom', presentation: 'modal' }}
/>
<Stack.Screen
name="Notifications"
component={NotificationsScreen}
options={{ animation: 'slide_from_right' }}
/>
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{ animation: 'slide_from_right' }}
/>
<Stack.Screen
name="Settings"
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>
</NavigationContainer>
);

View file

@ -10,15 +10,11 @@ const linking: LinkingOptions<RootStackParamList> = {
screens: {
Home: 'home',
Scanner: 'scan',
Map: 'map',
History: 'history',
Profile: 'profile',
},
},
Result: 'result',
Notifications: 'notifications',
Profile: 'profile',
Settings: 'settings',
Guides: 'guides',
Library: 'library',
},
},
};

View file

@ -1,373 +0,0 @@
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,59 +1,214 @@
import { View, ScrollView } from "react-native";
import { useEffect } from "react";
import { View, ScrollView, TouchableOpacity, TextInput } 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 type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
import { useTranslation } from "react-i18next";
import { Ionicons } from "@expo/vector-icons";
import Animated, {
useSharedValue,
useAnimatedStyle,
withRepeat,
withSequence,
withTiming,
} from "react-native-reanimated";
import type { RootStackParamList } from "@/types/navigation";
import SearchHeader from "@/components/home/SearchHeader";
import SearchSection from "@/components/home/SearchSection";
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 { BottomTabParamList } from "@/types/navigation";
import StatCard from "@/components/home/gamificationstat";
import StatisticsSection from "@/components/home/statssection";
import SectionHeader from "@/components/home/components/homeheader";
import FrequentDiseases from "@/components/home/FrequentDiseases";
import SeasonAlert from "@/components/home/SeasonAlert";
import PracticalGuides from "@/components/home/PracticalGuides";
import HeroScanner from "@/components/home/HomeCta";
import MaterialIcons from "@expo/vector-icons/MaterialIcons";
type Nav = NativeStackNavigationProp<RootStackParamList>;
type HomeNav = BottomTabNavigationProp<BottomTabParamList, "Home">;
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,
},
];
export default function HomeScreen() {
const { t } = useTranslation();
const navigation = useNavigation<Nav>();
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);
return (
<SafeAreaView className="flex-1 bg-[#FAFAFA]" edges={["top"]}>
<ScrollView
className="flex-1"
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: 24 }}
contentContainerStyle={{
paddingBottom: 130, // La hauteur de ta barre (70) + le margin bas (~34) + de l'espace pour respirer (26)
}}
>
<SearchHeader />
<SearchSection />
<HeroScanner />
{/* Frequent diseases carousel */}
<View className="mb-6 gap-3">
<View className="px-5">
<SectionHeader
title={t("home.frequentDiseases")}
onViewAll={() => navigation.navigate("Guides")}
{/* Header — title left, icons right */}
<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>
<FrequentDiseases />
</View>
{/* Season alert */}
<SeasonAlert />
{/* Practical guides */}
<View className="mx-5 mb-6 gap-3">
<SectionHeader
title={t("home.practicalGuides")}
onViewAll={() => navigation.navigate("Guides")}
<TouchableOpacity
className="h-9 w-9 items-center justify-center rounded-full bg-neutral-200"
activeOpacity={0.7}
>
<Ionicons
name="add-outline"
size={22}
color={colors.neutral[800]}
/>
<PracticalGuides />
</TouchableOpacity>
</View>
</View>
<StatisticsSection progress={userProgress} />
{/* 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]} />
</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>
</View>
{/* Last scan section */}
{lastScan && (
<View className="mx-5 mb-6 gap-2">
<SectionHeader
title={t("home.lastScan")}
onViewAll={() => navigation.navigate("History")}
/>
<ScanCard record={lastScan} />
</View>
)}
<View className="h-8" />
</ScrollView>

View file

@ -1,296 +0,0 @@
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

@ -1,30 +0,0 @@
import { View } 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";
export default function MapScreen() {
const { t } = useTranslation();
return (
<SafeAreaView className="flex-1 bg-[#FAFAFA]" edges={["top"]}>
<View className="flex-1 items-center justify-center px-8">
<View
className="mb-6 h-20 w-20 items-center justify-center rounded-full"
style={{ backgroundColor: colors.primary[100] }}
>
<Ionicons name="map-outline" size={36} color={colors.primary[700]} />
</View>
<Text className="mb-2 text-xl font-semibold" style={{ color: colors.neutral[900] }}>
{t("common.map")}
</Text>
<Text className="text-center text-sm" style={{ color: colors.neutral[500] }}>
Coming soon
</Text>
</View>
</SafeAreaView>
);
}

View file

@ -1,424 +0,0 @@
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,253 +1,208 @@
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 { 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 { 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" },
];
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';
export default function ProfileScreen() {
const { t } = useTranslation();
const navigation = useNavigation<Nav>();
const { progress } = useGameProgress();
function handleBack() {
if (navigation.canGoBack()) {
navigation.goBack();
} else {
navigation.navigate("Main" as any);
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();
},
},
]);
}
return (
<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}>
<SafeAreaView style={styles.safe} edges={['top']}>
<ScrollView showsVerticalScrollIndicator={false}>
{/* Header */}
<View style={styles.header}>
<View style={styles.avatar}>
<Text style={styles.avatarEmoji}>🧑🌾</Text>
</View>
<Text style={styles.avatarText}>🧑🌾</Text>
</View>
<Text style={styles.username}>Vigneron</Text>
<Text style={styles.xpTotal}>{progress.xp} XP</Text>
</View>
{/* 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>
{/* XP Bar */}
<Card style={styles.section} variant="elevated">
<XPBar xp={progress.xp} />
</Card>
<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>
</View>
{/* Stats Grid - Bento Style Pur */}
{/* Stats */}
<Card style={styles.section} variant="elevated">
<Text style={styles.sectionTitle}>{t('profile.stats')}</Text>
<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 style={styles.statItem}>
<Text style={styles.statValue}>{progress.totalScans}</Text>
<Text style={styles.statLabel}>{t('profile.totalScans')}</Text>
</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 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>
</View>
</Card>
{/* 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>
<View style={{ height: 60 }} />
{/* 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'] }} />
</ScrollView>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
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 },
}),
safe: { flex: 1, backgroundColor: colors.background },
header: {
alignItems: 'center',
paddingVertical: spacing['2xl'],
gap: spacing.sm,
},
avatar: {
width: 96,
height: 96,
borderRadius: 48,
backgroundColor: colors.primary[50],
alignItems: "center",
justifyContent: "center",
width: 80,
height: 80,
borderRadius: 40,
backgroundColor: colors.primary[200],
alignItems: 'center',
justifyContent: 'center',
},
avatarEmoji: {
fontSize: 48,
avatarText: { fontSize: 40 },
username: {
fontSize: typography.fontSizes.xl,
fontWeight: typography.fontWeights.bold,
color: colors.neutral[900],
},
infoCard: {
backgroundColor: "#FFFFFF",
borderRadius: 32,
padding: 24,
alignItems: "center",
marginBottom: 20,
borderWidth: 1,
borderColor: "#F0F0F0",
xpTotal: {
fontSize: typography.fontSizes.sm,
color: colors.primary[700],
fontWeight: typography.fontWeights.semibold,
},
userName: {
fontSize: 24,
fontWeight: "800",
color: "#1A1A1A",
letterSpacing: -0.5,
section: {
marginHorizontal: spacing.base,
marginBottom: spacing.md,
gap: spacing.md,
},
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",
sectionTitle: {
fontSize: typography.fontSizes.md,
fontWeight: typography.fontWeights.semibold,
color: colors.neutral[800],
marginBottom: spacing.xs,
},
statsGrid: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between",
flexDirection: 'row',
flexWrap: 'wrap',
gap: spacing.sm,
},
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,
statItem: {
width: '47%',
alignItems: 'center',
backgroundColor: colors.neutral[100],
borderRadius: 12,
paddingVertical: spacing.md,
gap: spacing.xs,
},
statValue: {
fontSize: 22,
fontWeight: "500", // Medium au lieu de Bold pour le look premium
color: "#1A1A1A",
fontSize: typography.fontSizes.xl,
fontWeight: typography.fontWeights.bold,
color: colors.primary[800],
},
statLabel: {
fontSize: 13,
color: "#9A9A9A",
marginTop: 4,
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],
},
});

View file

@ -1,320 +0,0 @@
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

@ -1,5 +1,5 @@
import { useEffect } from 'react';
import { View, Text, Image, StyleSheet } from 'react-native';
import { View, Text, StyleSheet } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { colors } from '@/theme/colors';
@ -21,13 +21,9 @@ export default function SplashScreen() {
return (
<View style={styles.container}>
<Image
source={require('@/assets/images/icon.png')}
style={styles.logoImage}
resizeMode="contain"
/>
{/* <Text style={styles.logo}>VinEye</Text>
<Text style={styles.subtitle}>Détection de vignes par IA</Text> */}
<Text style={styles.leafEmoji}>🍃</Text>
<Text style={styles.logo}>VinEye</Text>
<Text style={styles.subtitle}>Détection de vignes par IA</Text>
</View>
);
}
@ -35,15 +31,12 @@ export default function SplashScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.surface,
backgroundColor: colors.primary[900],
alignItems: 'center',
justifyContent: 'center',
},
logoImage: {
width: 198,
height: 198,
gap: 24,
},
leafEmoji: { fontSize: 80 },
logo: {
fontSize: typography.fontSizes['4xl'],
fontWeight: typography.fontWeights.extrabold,

View file

@ -4,17 +4,11 @@ export type RootStackParamList = {
Splash: undefined;
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;
History: undefined;
Profile: undefined;
};