Compare commits

..

2 commits

Author SHA1 Message Date
Yanis af299e816a add new screens + home components + replace colors with theme tokens
New screens: Guides, Library, Map, Notifications, Settings.
Home refactored with modular components (SearchHeader, HomeCta, FrequentDiseases, SeasonAlert, PracticalGuides).
Replaced hardcoded colors with theme tokens in SeasonAlert and HomeCta.
Updated navigation, i18n, and CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 22:05:19 +02:00
Yanis 001658898e refactor navigation: classic bottom tab bar with FAB + header icons
- Replace floating pill tab bar with classic anchored bottom bar (Home | FAB Scan | Map)
- Add central FAB button (green, elevated) for Scanner
- Move History → Notifications and Profile → Settings (accessible via header icons)
- Add SearchHeader with bell (notifications) and settings icons
- Add MapScreen placeholder
- Extract SearchHeader component from HomeScreen
- Switch to lucide-react-native icons for bottom tab bar
- Fix react-dom version mismatch (19.2.4 → 19.1.0)
- Clean up unused imports in homeheader.tsx
- Update navigation types, deep links, and i18n keys

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-02 20:14:27 +02:00
32 changed files with 3554 additions and 598 deletions

3
.gitignore vendored
View file

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

65
AGENTS.md Normal file
View file

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

BIN
VinEye/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

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

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

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

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

View file

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

View file

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

View file

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

View file

@ -3,6 +3,11 @@ import { NavigationContainer } from '@react-navigation/native';
import SplashScreen from '@/screens/SplashScreen'; import SplashScreen from '@/screens/SplashScreen';
import ResultScreen from '@/screens/ResultScreen'; 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 BottomTabNavigator from './BottomTabNavigator';
import linking from './linking'; import linking from './linking';
import type { RootStackParamList } from '@/types/navigation'; import type { RootStackParamList } from '@/types/navigation';
@ -23,6 +28,31 @@ export default function RootNavigator() {
component={ResultScreen} component={ResultScreen}
options={{ animation: 'slide_from_bottom', presentation: 'modal' }} 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> </Stack.Navigator>
</NavigationContainer> </NavigationContainer>
); );

View file

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

View file

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

View file

@ -1,214 +1,59 @@
import { useEffect } from "react"; import { View, ScrollView } from "react-native";
import { View, ScrollView, TouchableOpacity, TextInput } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context"; import { SafeAreaView } from "react-native-safe-area-context";
import { useNavigation } from "@react-navigation/native"; import { useNavigation } from "@react-navigation/native";
import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs"; import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useTranslation } from "react-i18next"; 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 type { RootStackParamList } from "@/types/navigation";
import { ProgressRing } from "@/components/gamification/ProgressRing"; import SearchHeader from "@/components/home/SearchHeader";
import { ScanCard } from "@/components/history/ScanCard"; import SearchSection from "@/components/home/SearchSection";
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 SectionHeader from "@/components/home/components/homeheader";
import MaterialIcons from "@expo/vector-icons/MaterialIcons"; import FrequentDiseases from "@/components/home/FrequentDiseases";
import SeasonAlert from "@/components/home/SeasonAlert";
import PracticalGuides from "@/components/home/PracticalGuides";
import HeroScanner from "@/components/home/HomeCta";
type HomeNav = BottomTabNavigationProp<BottomTabParamList, "Home">; type Nav = NativeStackNavigationProp<RootStackParamList>;
interface GameProgress {
totalScans: number;
uniqueGrapes: string[];
streak: number;
}
const userProgress = {
streak: 12, // La série de jours
xpTotal: 2450, // Le total de points XP
// ... tes autres données
};
const STAT_CARDS: {
labelKey: string;
icon: keyof typeof Ionicons.glyphMap;
bg: string;
accent: string;
dark: string;
getValue: (p: GameProgress) => number;
}[] = [
{
labelKey: "home.totalScans",
icon: "scan-outline",
bg: "#E8F0EA",
accent: "#2D6A4F",
dark: "#1B4332",
getValue: (p) => p.totalScans,
},
{
labelKey: "home.uniqueGrapes",
icon: "leaf-outline",
bg: "#EBE5F6",
accent: "#7B5EA7",
dark: "#3E0047",
getValue: (p) => p.uniqueGrapes.length,
},
{
labelKey: "home.currentStreak",
icon: "flame-outline",
bg: "#F0EBE3",
accent: "#8B7355",
dark: "#4A3F30",
getValue: (p) => p.streak,
},
];
export default function HomeScreen() { export default function HomeScreen() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigation = useNavigation<HomeNav>(); const navigation = useNavigation<Nav>();
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 ( return (
<SafeAreaView className="flex-1 bg-[#FAFAFA]" edges={["top"]}> <SafeAreaView className="flex-1 bg-[#FAFAFA]" edges={["top"]}>
<ScrollView <ScrollView
className="flex-1" className="flex-1"
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
contentContainerStyle={{ contentContainerStyle={{ paddingBottom: 24 }}
paddingBottom: 130, // La hauteur de ta barre (70) + le margin bas (~34) + de l'espace pour respirer (26)
}}
> >
{/* Header — title left, icons right */} <SearchHeader />
<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>
<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]}
/>
</TouchableOpacity>
</View>
</View>
<StatisticsSection progress={userProgress} /> <SearchSection />
{/* Scan banner */} <HeroScanner />
<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 {/* Frequent diseases carousel */}
className="mb-1 text-[18px] font-bold" <View className="mb-6 gap-3">
style={{ color: colors.primary[900] }} <View className="px-5">
>
{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 <SectionHeader
title={t("home.lastScan")} title={t("home.frequentDiseases")}
onViewAll={() => navigation.navigate("History")} onViewAll={() => navigation.navigate("Guides")}
/> />
<ScanCard record={lastScan} />
</View> </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")}
/>
<PracticalGuides />
</View>
<View className="h-8" /> <View className="h-8" />
</ScrollView> </ScrollView>

View file

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

View file

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

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

View file

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

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

View file

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

View file

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