Compare commits
2 commits
a964cc3836
...
af299e816a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af299e816a | ||
|
|
001658898e |
3
.gitignore
vendored
|
|
@ -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
|
|
@ -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`.
|
||||||
189
VinEye/CLAUDE.md
|
|
@ -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
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 4.4 KiB |
149
VinEye/src/components/home/FrequentDiseases.tsx
Normal 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],
|
||||||
|
}
|
||||||
|
});
|
||||||
191
VinEye/src/components/home/HomeCta.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
112
VinEye/src/components/home/PracticalGuides.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
117
VinEye/src/components/home/SearchHeader.tsx
Normal 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",
|
||||||
|
},
|
||||||
|
});
|
||||||
79
VinEye/src/components/home/SearchSection.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
104
VinEye/src/components/home/SeasonAlert.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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
|
|
@ -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
|
|
@ -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",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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'
|
size={22}
|
||||||
}`}
|
color={tintColor}
|
||||||
style={{ borderRadius: 999 }}
|
strokeWidth={isFocused ? 2.5 : 1.8}
|
||||||
>
|
|
||||||
<Ionicons
|
|
||||||
name={getIcon(route.name, isFocused) as any}
|
|
||||||
size={22}
|
|
||||||
color={isFocused ? '#FFFFFF' : '#9CA3AF'}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{isFocused && (
|
{/* <Text
|
||||||
<Text
|
numberOfLines={1}
|
||||||
numberOfLines={1}
|
style={{
|
||||||
className="ml-2 text-white font-bold text-[13px]"
|
fontSize: 11,
|
||||||
>
|
marginTop: 4,
|
||||||
{label}
|
color: tintColor,
|
||||||
</Text>
|
fontWeight: isFocused ? "600" : "400",
|
||||||
)}
|
}}
|
||||||
</View>
|
>
|
||||||
|
{label}
|
||||||
|
</Text> */}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
@ -123,26 +148,31 @@ export default function BottomTabNavigator() {
|
||||||
tabBar={(props) => <MyCustomTabBar {...props} />}
|
tabBar={(props) => <MyCustomTabBar {...props} />}
|
||||||
screenOptions={{ headerShown: false }}
|
screenOptions={{ headerShown: false }}
|
||||||
>
|
>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
373
VinEye/src/screens/GuidesScreen.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
296
VinEye/src/screens/LibraryScreen.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
30
VinEye/src/screens/MapScreen.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
424
VinEye/src/screens/NotificationsScreen.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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.avatar}>
|
<View style={styles.heroTopRow}>
|
||||||
<Text style={styles.avatarText}>🧑🌾</Text>
|
<TouchableOpacity onPress={handleBack} style={styles.heroBackBtn}>
|
||||||
|
<Ionicons name="chevron-back" size={24} color="#FFFFFF" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<TouchableOpacity onPress={() => navigation.navigate("Settings")} style={styles.heroSettingsBtn}>
|
||||||
|
<Ionicons name="settings-outline" size={22} color={colors.primary[800]} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView style={styles.scrollView} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false}>
|
||||||
|
|
||||||
|
{/* Avatar avec bague de séparation */}
|
||||||
|
<View style={styles.avatarContainer}>
|
||||||
|
<View style={styles.avatarRing}>
|
||||||
|
<View style={styles.avatar}>
|
||||||
|
<Text style={styles.avatarEmoji}>🧑🌾</Text>
|
||||||
|
</View>
|
||||||
</View>
|
</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>
|
||||||
<View style={styles.statsGrid}>
|
</TouchableOpacity>
|
||||||
<View style={styles.statItem}>
|
<View style={styles.xpBadge}>
|
||||||
<Text style={styles.statValue}>{progress.totalScans}</Text>
|
<Text style={styles.xpBadgeText}>{progress.xp} XP</Text>
|
||||||
<Text style={styles.statLabel}>{t('profile.totalScans')}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<Text style={styles.statValue}>{progress.uniqueGrapes.length}</Text>
|
|
||||||
<Text style={styles.statLabel}>{t('profile.uniqueGrapes')}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<Text style={[styles.statValue, { color: colors.warning }]}>{progress.bestStreak}</Text>
|
|
||||||
<Text style={styles.statLabel}>{t('profile.bestStreak')}</Text>
|
|
||||||
</View>
|
|
||||||
<View style={styles.statItem}>
|
|
||||||
<Text style={styles.statValue}>{progress.streak}</Text>
|
|
||||||
<Text style={styles.statLabel}>{t('home.currentStreak')}</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</Card>
|
</View>
|
||||||
|
|
||||||
{/* Badges */}
|
{/* Stats Grid - Bento Style Pur */}
|
||||||
<Card style={styles.section} variant="elevated">
|
<View style={styles.statsGrid}>
|
||||||
<Text style={styles.sectionTitle}>{t('profile.badges')}</Text>
|
{BENTO_STATS.map((stat) => (
|
||||||
<View style={styles.badgesGrid}>
|
<View key={stat.key} style={styles.statCard}>
|
||||||
{progress.badges.map((badge) => (
|
<View style={[styles.statIconWrap, { backgroundColor: `${stat.iconColor}15` }]}>
|
||||||
<BadgeCard key={badge.id} badge={badge} />
|
<Ionicons name={stat.icon as any} size={22} color={stat.iconColor} />
|
||||||
))}
|
</View>
|
||||||
</View>
|
<Text style={styles.statValue}>
|
||||||
</Card>
|
{stat.key === "grapes" ? (progress.uniqueGrapes?.length ?? 0) : progress[stat.key as keyof typeof progress] || 0}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.statLabel}>{t(stat.label)}</Text>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
|
||||||
{/* Settings */}
|
<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],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
320
VinEye/src/screens/SettingsScreen.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
||||||