diff --git a/mobile/package.json b/mobile/package.json
index db3dde9..c2d6001 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -30,6 +30,7 @@
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
+ "react-native-svg": "^15.15.1",
"react-native-worklets": "^0.5.1"
},
"devDependencies": {
diff --git a/mobile/src/config/.gitkeep b/mobile/src/config/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/mobile/src/navigation/Navigation.tsx b/mobile/src/navigation/Navigation.tsx
index 9eb758f..a4dbe45 100644
--- a/mobile/src/navigation/Navigation.tsx
+++ b/mobile/src/navigation/Navigation.tsx
@@ -15,6 +15,11 @@ import RegisterScreen from '@screens/RegisterScreen';
import MapScreen from '@screens/MapScreen';
import WalkHistoryScreen from '@screens/WalkHistoryScreen';
import WalkDetailScreen from '@screens/WalkDetailScreen';
+import HealthScreen from '@screens/HealthScreen';
+import VisitsScreen from '@screens/VisitsScreen';
+import MedicationsScreen from '@screens/MedicationsScreen';
+import VaccinationsScreen from '@screens/VaccinationsScreen';
+import WeightManagementScreen from '@screens/WeightManagementScreen';
const Stack = createNativeStackNavigator();
const Tab = createBottomTabNavigator();
@@ -175,6 +180,46 @@ export default function Navigation() {
headerBackTitle: 'Takaisin',
}}
/>
+
+
+
+
+
>
) : (
<>
diff --git a/mobile/src/screens/HealthScreen.tsx b/mobile/src/screens/HealthScreen.tsx
new file mode 100644
index 0000000..523a468
--- /dev/null
+++ b/mobile/src/screens/HealthScreen.tsx
@@ -0,0 +1,80 @@
+import React, { useEffect, useRef } from 'react';
+import { View, ScrollView, Animated } from 'react-native';
+import { Text, Card } from 'react-native-paper';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { useNavigation } from '@react-navigation/native';
+import { homeStyles as styles } from '../styles/screenStyles';
+import { COLORS, SPACING } from '../styles/theme';
+
+export default function HealthScreen() {
+ const navigation = useNavigation();
+ const pulse = useRef(new Animated.Value(1)).current;
+
+ useEffect(() => {
+ const animatePulse = () => {
+ Animated.sequence([
+ Animated.timing(pulse, { toValue: 1.2, duration: 800, useNativeDriver: true }),
+ Animated.timing(pulse, { toValue: 1, duration: 800, useNativeDriver: true }),
+ Animated.delay(500),
+ ]).start(() => animatePulse());
+ };
+ animatePulse();
+ }, []);
+
+ return (
+
+
+
+
+
+
+
+
+ Pidä huolta lemmikin terveydestä
+
+
+
+
+ navigation.navigate('Visits' as never)}>
+
+
+ Käynnit
+
+ Eläinlääkäri- ja klinikkakäynnit
+
+
+
+
+ navigation.navigate('Medications' as never)}>
+
+
+ Lääkitykset
+
+ Lääkkeet ja hoito-ohjelmat
+
+
+
+
+ navigation.navigate('Vaccinations' as never)}>
+
+
+ Rokotukset
+
+ Rokotushistoria ja muistutukset
+
+
+
+
+ navigation.navigate('WeightManagement' as never)}>
+
+
+ Painonhallinta
+
+ Seuraa painoa ja kasvua
+
+
+
+
+
+ );
+}
diff --git a/mobile/src/screens/HomeScreen.tsx b/mobile/src/screens/HomeScreen.tsx
index 3fecc87..7a12aa7 100644
--- a/mobile/src/screens/HomeScreen.tsx
+++ b/mobile/src/screens/HomeScreen.tsx
@@ -82,7 +82,7 @@ export default function HomeScreen() {
- console.log('Terveys')}>
+ navigation.navigate('Health' as never)}>
Terveys
diff --git a/mobile/src/screens/MedicationsScreen.tsx b/mobile/src/screens/MedicationsScreen.tsx
new file mode 100644
index 0000000..d3713a7
--- /dev/null
+++ b/mobile/src/screens/MedicationsScreen.tsx
@@ -0,0 +1,347 @@
+import React, { useState } from 'react';
+import { View, ScrollView, StyleSheet } from 'react-native';
+import { Text, Card, FAB, Chip, Divider } from 'react-native-paper';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { COLORS, SPACING } from '../styles/theme';
+
+interface Pet {
+ id: string;
+ name: string;
+ breed: string;
+ age: number;
+}
+
+interface Medication {
+ id: string;
+ petId: string;
+ name: string;
+ dosage: string;
+ frequency: string;
+ startDate: string;
+ endDate?: string;
+ purpose: string;
+ prescribedBy: string;
+ notes?: string;
+ isActive: boolean;
+}
+
+export default function MedicationsScreen() {
+ // Mock data - replace with actual data from context/API
+ const [pets] = useState([]);
+
+ const [medications] = useState([]);
+
+ const [selectedPetId, setSelectedPetId] = useState(pets[0]?.id || '');
+
+ const selectedPetMedications = medications
+ .filter(med => med.petId === selectedPetId)
+ .sort((a, b) => {
+ if (a.isActive === b.isActive) {
+ return new Date(b.startDate).getTime() - new Date(a.startDate).getTime();
+ }
+ return a.isActive ? -1 : 1;
+ });
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('fi-FI', {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ });
+ };
+
+ const renderMedicationCard = (medication: Medication) => (
+
+
+
+
+ {medication.name}
+
+
+ {medication.isActive ? 'Aktiivinen' : 'Päättynyt'}
+
+
+
+
+
+
+ {medication.dosage} - {medication.frequency}
+
+
+
+
+
+
+
+
+ {medication.purpose}
+
+
+
+
+
+
+ {medication.prescribedBy}
+
+
+
+
+
+
+ Aloitettu: {formatDate(medication.startDate)}
+
+
+
+ {medication.endDate && (
+
+
+
+ Päättynyt: {formatDate(medication.endDate)}
+
+
+ )}
+
+ {medication.notes && (
+ <>
+
+
+
+
+ {medication.notes}
+
+
+ >
+ )}
+
+
+ );
+
+ const renderEmptyState = () => (
+
+
+
+ Ei lääkityksiä
+
+
+ Lisää ensimmäinen lääkitys
+
+
+ );
+
+ if (pets.length === 0) {
+ return (
+
+
+
+
+ Ei lemmikkejä
+
+
+ Lisää ensin lemmikki
+
+
+
+ );
+ }
+
+ return (
+
+
+ {pets.map((pet) => (
+ setSelectedPetId(pet.id)}
+ style={[
+ styles.tab,
+ selectedPetId === pet.id && styles.selectedTab
+ ]}
+ textStyle={selectedPetId === pet.id ? styles.selectedTabText : styles.unselectedTabText}
+ icon={() => (
+
+ )}
+ >
+ {pet.name}
+
+ ))}
+
+
+
+ {selectedPetMedications.length === 0 ? (
+ renderEmptyState()
+ ) : (
+ selectedPetMedications.map(renderMedicationCard)
+ )}
+
+
+ console.log('Lisää lääkitys')}
+ label="Lisää lääkitys"
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.background,
+ },
+ header: {
+ paddingHorizontal: SPACING.lg,
+ paddingTop: SPACING.lg,
+ paddingBottom: SPACING.md,
+ },
+ title: {
+ fontWeight: 'bold',
+ color: COLORS.onBackground,
+ },
+ tabsContainer: {
+ maxHeight: 70,
+ backgroundColor: COLORS.surface,
+ borderBottomWidth: 1,
+ borderBottomColor: COLORS.surfaceVariant,
+ },
+ tabsContent: {
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.md,
+ gap: SPACING.sm,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexGrow: 1,
+ },
+ tab: {
+ marginHorizontal: SPACING.xs,
+ paddingHorizontal: SPACING.md,
+ height: 40,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ selectedTab: {
+ backgroundColor: COLORS.primary,
+ elevation: 3,
+ },
+ selectedTabText: {
+ color: '#FFFFFF',
+ fontWeight: '700',
+ fontSize: 15,
+ lineHeight: 20,
+ },
+ unselectedTabText: {
+ fontSize: 15,
+ lineHeight: 20,
+ },
+ content: {
+ flex: 1,
+ },
+ scrollContent: {
+ padding: SPACING.md,
+ },
+ medicationCard: {
+ marginBottom: SPACING.md,
+ elevation: 2,
+ },
+ medicationHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: SPACING.sm,
+ },
+ medicationName: {
+ fontWeight: '700',
+ color: COLORS.primary,
+ flex: 1,
+ },
+ statusChip: {
+ marginLeft: SPACING.sm,
+ },
+ activeChip: {
+ backgroundColor: '#E8F5E9',
+ },
+ inactiveChip: {
+ backgroundColor: COLORS.surfaceVariant,
+ },
+ activeChipText: {
+ color: '#2E7D32',
+ },
+ inactiveChipText: {
+ color: COLORS.onSurfaceVariant,
+ },
+ dosageContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.xs,
+ marginBottom: SPACING.sm,
+ },
+ dosage: {
+ fontWeight: '600',
+ color: COLORS.onSurface,
+ },
+ divider: {
+ marginVertical: SPACING.sm,
+ },
+ medicationDetail: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.sm,
+ marginBottom: SPACING.xs,
+ },
+ detailText: {
+ flex: 1,
+ color: COLORS.onSurface,
+ },
+ notesContainer: {
+ flexDirection: 'row',
+ gap: SPACING.sm,
+ marginTop: SPACING.xs,
+ },
+ notesText: {
+ flex: 1,
+ color: COLORS.onSurfaceVariant,
+ fontStyle: 'italic',
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: SPACING.xl * 2,
+ },
+ emptyTitle: {
+ marginTop: SPACING.md,
+ color: COLORS.onSurfaceVariant,
+ },
+ emptyText: {
+ marginTop: SPACING.xs,
+ color: COLORS.onSurfaceVariant,
+ textAlign: 'center',
+ },
+ fab: {
+ position: 'absolute',
+ right: SPACING.md,
+ bottom: SPACING.md,
+ backgroundColor: COLORS.primary,
+ },
+});
diff --git a/mobile/src/screens/VaccinationsScreen.tsx b/mobile/src/screens/VaccinationsScreen.tsx
new file mode 100644
index 0000000..b6c3863
--- /dev/null
+++ b/mobile/src/screens/VaccinationsScreen.tsx
@@ -0,0 +1,369 @@
+import React, { useState } from 'react';
+import { View, ScrollView, StyleSheet } from 'react-native';
+import { Text, Card, FAB, Chip, Divider } from 'react-native-paper';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { COLORS, SPACING } from '../styles/theme';
+
+interface Pet {
+ id: string;
+ name: string;
+ breed: string;
+ age: number;
+}
+
+interface Vaccination {
+ id: string;
+ petId: string;
+ name: string;
+ date: string;
+ nextDueDate?: string;
+ batchNumber?: string;
+ veterinarian: string;
+ clinic: string;
+ notes?: string;
+ isUpToDate: boolean;
+}
+
+export default function VaccinationsScreen() {
+ // Mock data - replace with actual data from context/API
+ const [pets] = useState([]);
+
+ const [vaccinations] = useState([]);
+
+ const [selectedPetId, setSelectedPetId] = useState(pets[0]?.id || '');
+
+ const selectedPetVaccinations = vaccinations
+ .filter(vac => vac.petId === selectedPetId)
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('fi-FI', {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ });
+ };
+
+ const isOverdue = (nextDueDate?: string) => {
+ if (!nextDueDate) return false;
+ return new Date(nextDueDate) < new Date();
+ };
+
+ const renderVaccinationCard = (vaccination: Vaccination) => {
+ const overdue = isOverdue(vaccination.nextDueDate);
+
+ return (
+
+
+
+
+ {vaccination.name}
+
+
+ {overdue ? 'Myöhässä' : vaccination.isUpToDate ? 'Voimassa' : 'Odottaa'}
+
+
+
+
+
+
+ Annettu: {formatDate(vaccination.date)}
+
+
+
+ {vaccination.nextDueDate && (
+
+
+
+ Voimassa: {formatDate(vaccination.nextDueDate)}
+
+
+ )}
+
+
+
+
+
+
+ {vaccination.clinic}
+
+
+
+
+
+
+ {vaccination.veterinarian}
+
+
+
+ {vaccination.notes && (
+ <>
+
+
+
+
+ {vaccination.notes}
+
+
+ >
+ )}
+
+
+ );
+ };
+
+ const renderEmptyState = () => (
+
+
+
+ Ei rokotuksia
+
+
+ Lisää ensimmäinen rokotus
+
+
+ );
+
+ if (pets.length === 0) {
+ return (
+
+
+
+
+ Ei lemmikkejä
+
+
+ Lisää ensin lemmikki
+
+
+
+ );
+ }
+
+ return (
+
+
+ {pets.map((pet) => (
+ setSelectedPetId(pet.id)}
+ style={[
+ styles.tab,
+ selectedPetId === pet.id && styles.selectedTab
+ ]}
+ textStyle={selectedPetId === pet.id ? styles.selectedTabText : styles.unselectedTabText}
+ icon={() => (
+
+ )}
+ >
+ {pet.name}
+
+ ))}
+
+
+
+ {selectedPetVaccinations.length === 0 ? (
+ renderEmptyState()
+ ) : (
+ selectedPetVaccinations.map(renderVaccinationCard)
+ )}
+
+
+ console.log('Lisää rokotus')}
+ label="Lisää rokotus"
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.background,
+ },
+ header: {
+ paddingHorizontal: SPACING.lg,
+ paddingTop: SPACING.lg,
+ paddingBottom: SPACING.md,
+ },
+ title: {
+ fontWeight: 'bold',
+ color: COLORS.onBackground,
+ },
+ tabsContainer: {
+ maxHeight: 70,
+ backgroundColor: COLORS.surface,
+ borderBottomWidth: 1,
+ borderBottomColor: COLORS.surfaceVariant,
+ },
+ tabsContent: {
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.md,
+ gap: SPACING.sm,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexGrow: 1,
+ },
+ tab: {
+ marginHorizontal: SPACING.xs,
+ paddingHorizontal: SPACING.md,
+ height: 40,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ selectedTab: {
+ backgroundColor: COLORS.primary,
+ elevation: 3,
+ },
+ selectedTabText: {
+ color: '#FFFFFF',
+ fontWeight: '700',
+ fontSize: 15,
+ lineHeight: 20,
+ },
+ unselectedTabText: {
+ fontSize: 15,
+ lineHeight: 20,
+ },
+ content: {
+ flex: 1,
+ },
+ scrollContent: {
+ padding: SPACING.md,
+ },
+ vaccinationCard: {
+ marginBottom: SPACING.md,
+ elevation: 2,
+ },
+ vaccinationHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: SPACING.sm,
+ },
+ vaccinationName: {
+ fontWeight: '700',
+ color: COLORS.primary,
+ flex: 1,
+ },
+ statusChip: {
+ marginLeft: SPACING.sm,
+ },
+ upToDateChip: {
+ backgroundColor: '#E8F5E9',
+ },
+ pendingChip: {
+ backgroundColor: '#FFF8E1',
+ },
+ overdueChip: {
+ backgroundColor: '#FFEBEE',
+ },
+ upToDateChipText: {
+ color: '#2E7D32',
+ },
+ pendingChipText: {
+ color: '#F57C00',
+ },
+ overdueChipText: {
+ color: '#D32F2F',
+ },
+ dateContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.xs,
+ },
+ dateText: {
+ fontWeight: '600',
+ color: COLORS.onSurface,
+ },
+ nextDueText: {
+ color: COLORS.onSurfaceVariant,
+ },
+ overdueText: {
+ color: '#D32F2F',
+ fontWeight: '600',
+ },
+ divider: {
+ marginVertical: SPACING.sm,
+ },
+ vaccinationDetail: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.sm,
+ marginBottom: SPACING.xs,
+ },
+ detailText: {
+ flex: 1,
+ color: COLORS.onSurface,
+ },
+ notesContainer: {
+ flexDirection: 'row',
+ gap: SPACING.sm,
+ marginTop: SPACING.xs,
+ },
+ notesText: {
+ flex: 1,
+ color: COLORS.onSurfaceVariant,
+ fontStyle: 'italic',
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: SPACING.xl * 2,
+ },
+ emptyTitle: {
+ marginTop: SPACING.md,
+ color: COLORS.onSurfaceVariant,
+ },
+ emptyText: {
+ marginTop: SPACING.xs,
+ color: COLORS.onSurfaceVariant,
+ textAlign: 'center',
+ },
+ fab: {
+ position: 'absolute',
+ right: SPACING.md,
+ bottom: SPACING.md,
+ backgroundColor: COLORS.primary,
+ },
+});
diff --git a/mobile/src/screens/VisitsScreen.tsx b/mobile/src/screens/VisitsScreen.tsx
new file mode 100644
index 0000000..ffc7547
--- /dev/null
+++ b/mobile/src/screens/VisitsScreen.tsx
@@ -0,0 +1,299 @@
+import React, { useState } from 'react';
+import { View, ScrollView, StyleSheet } from 'react-native';
+import { Text, Card, FAB, Chip, Divider } from 'react-native-paper';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import { COLORS, SPACING } from '../styles/theme';
+
+interface Pet {
+ id: string;
+ name: string;
+ breed: string;
+ age: number;
+}
+
+interface Visit {
+ id: string;
+ petId: string;
+ date: string;
+ clinic: string;
+ veterinarian: string;
+ reason: string;
+ notes?: string;
+ cost?: number;
+}
+
+export default function VisitsScreen() {
+ // Mock data - replace with actual data from context/API
+ const [pets] = useState([]);
+
+ const [visits] = useState([]);
+
+ const [selectedPetId, setSelectedPetId] = useState(pets[0]?.id || '');
+
+ const selectedPetVisits = visits
+ .filter(visit => visit.petId === selectedPetId)
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('fi-FI', {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ });
+ };
+
+ const renderVisitCard = (visit: Visit) => (
+
+
+
+
+
+
+ {formatDate(visit.date)}
+
+
+ {visit.cost && (
+
+ {visit.cost} €
+
+ )}
+
+
+
+
+
+
+
+ {visit.clinic}
+
+
+
+
+
+
+ {visit.veterinarian}
+
+
+
+
+
+
+ {visit.reason}
+
+
+
+ {visit.notes && (
+ <>
+
+
+
+
+ {visit.notes}
+
+
+ >
+ )}
+
+
+ );
+
+ const renderEmptyState = () => (
+
+
+
+ Ei käyntejä
+
+
+ Lisää ensimmäinen eläinlääkärikäynti
+
+
+ );
+
+ if (pets.length === 0) {
+ return (
+
+
+
+
+ Ei lemmikkejä
+
+
+ Lisää ensin lemmikki
+
+
+
+ );
+ }
+
+ return (
+
+
+ {pets.map((pet) => (
+ setSelectedPetId(pet.id)}
+ style={[
+ styles.tab,
+ selectedPetId === pet.id && styles.selectedTab
+ ]}
+ textStyle={selectedPetId === pet.id ? styles.selectedTabText : styles.unselectedTabText}
+ icon={() => (
+
+ )}
+ >
+ {pet.name}
+
+ ))}
+
+
+
+ {selectedPetVisits.length === 0 ? (
+ renderEmptyState()
+ ) : (
+ selectedPetVisits.map(renderVisitCard)
+ )}
+
+
+ console.log('Lisää käynti')}
+ label="Lisää käynti"
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.background,
+ },
+ header: {
+ paddingHorizontal: SPACING.lg,
+ paddingTop: SPACING.lg,
+ paddingBottom: SPACING.md,
+ },
+ title: {
+ fontWeight: 'bold',
+ color: COLORS.onBackground,
+ },
+ tabsContainer: {
+ maxHeight: 70,
+ backgroundColor: COLORS.surface,
+ borderBottomWidth: 1,
+ borderBottomColor: COLORS.surfaceVariant,
+ },
+ tabsContent: {
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.md,
+ gap: SPACING.sm,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexGrow: 1,
+ },
+ tab: {
+ marginHorizontal: SPACING.xs,
+ paddingHorizontal: SPACING.md,
+ height: 40,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ selectedTab: {
+ backgroundColor: COLORS.primary,
+ elevation: 3,
+ },
+ selectedTabText: {
+ color: '#FFFFFF',
+ fontWeight: '700',
+ fontSize: 15,
+ lineHeight: 20,
+ },
+ unselectedTabText: {
+ fontSize: 15,
+ lineHeight: 20,
+ },
+ content: {
+ flex: 1,
+ },
+ scrollContent: {
+ padding: SPACING.md,
+ },
+ visitCard: {
+ marginBottom: SPACING.md,
+ elevation: 2,
+ },
+ visitHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: SPACING.sm,
+ },
+ dateContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.xs,
+ },
+ dateText: {
+ fontWeight: '600',
+ color: COLORS.primary,
+ },
+ divider: {
+ marginVertical: SPACING.sm,
+ },
+ visitDetail: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.sm,
+ marginBottom: SPACING.xs,
+ },
+ detailText: {
+ flex: 1,
+ color: COLORS.onSurface,
+ },
+ notesContainer: {
+ flexDirection: 'row',
+ gap: SPACING.sm,
+ marginTop: SPACING.xs,
+ },
+ notesText: {
+ flex: 1,
+ color: COLORS.onSurfaceVariant,
+ fontStyle: 'italic',
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: SPACING.xl * 2,
+ },
+ emptyTitle: {
+ marginTop: SPACING.md,
+ color: COLORS.onSurfaceVariant,
+ },
+ emptyText: {
+ marginTop: SPACING.xs,
+ color: COLORS.onSurfaceVariant,
+ textAlign: 'center',
+ },
+ fab: {
+ position: 'absolute',
+ right: SPACING.md,
+ bottom: SPACING.md,
+ backgroundColor: COLORS.primary,
+ },
+});
diff --git a/mobile/src/screens/WeightManagementScreen.tsx b/mobile/src/screens/WeightManagementScreen.tsx
new file mode 100644
index 0000000..c912bec
--- /dev/null
+++ b/mobile/src/screens/WeightManagementScreen.tsx
@@ -0,0 +1,749 @@
+import React, { useState } from 'react';
+import { View, ScrollView, StyleSheet, Dimensions } from 'react-native';
+import { Text, Card, FAB, Chip, Divider } from 'react-native-paper';
+import { MaterialCommunityIcons } from '@expo/vector-icons';
+import Svg, { Line, Circle } from 'react-native-svg';
+import { COLORS, SPACING } from '../styles/theme';
+
+interface Pet {
+ id: string;
+ name: string;
+ breed: string;
+ age: number;
+}
+
+interface WeightRecord {
+ id: string;
+ petId: string;
+ date: string;
+ weight: number;
+ unit: 'kg' | 'g';
+ notes?: string;
+ measuredBy?: string;
+}
+
+export default function WeightManagementScreen() {
+ // Mock data - replace with actual data from context/API
+ const [pets] = useState([]);
+
+ const [weightRecords] = useState([]);
+
+ const [selectedPetId, setSelectedPetId] = useState(pets[0]?.id || '');
+ const [selectedYear, setSelectedYear] = useState(new Date().getFullYear());
+ const selectedPetWeights = weightRecords
+ .filter(record => record.petId === selectedPetId)
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+
+ // Get unique years from selected pet's weights
+ const availableYears = Array.from(
+ new Set(selectedPetWeights.map(record => new Date(record.date).getFullYear()))
+ ).sort((a, b) => a - b);
+
+ // Filter weights by selected year for graph
+ const yearFilteredWeights = selectedPetWeights.filter(
+ record => new Date(record.date).getFullYear() === selectedYear
+ );
+
+ const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleDateString('fi-FI', {
+ day: 'numeric',
+ month: 'long',
+ year: 'numeric',
+ });
+ };
+
+ const calculateWeightChange = (index: number) => {
+ if (index >= selectedPetWeights.length - 1) return null;
+
+ const current = selectedPetWeights[index].weight;
+ const previous = selectedPetWeights[index + 1].weight;
+ const change = current - previous;
+ const percentChange = ((change / previous) * 100).toFixed(1);
+
+ return { change: change.toFixed(1), percentChange, isIncrease: change > 0 };
+ };
+
+ const renderWeightCard = (record: WeightRecord, index: number) => {
+ const weightChange = calculateWeightChange(index);
+
+ return (
+
+
+
+
+
+ {record.weight}
+
+
+ {record.unit}
+
+
+ {weightChange && (
+
+ {weightChange.isIncrease ? '+' : ''}{weightChange.change} kg ({weightChange.percentChange}%)
+
+ )}
+
+
+
+
+
+ {formatDate(record.date)}
+
+
+
+ {record.measuredBy && (
+ <>
+
+
+
+
+ {record.measuredBy}
+
+
+ >
+ )}
+
+ {record.notes && (
+ <>
+
+
+
+
+ {record.notes}
+
+
+ >
+ )}
+
+
+ );
+ };
+
+ const renderEmptyState = () => (
+
+
+
+ Ei painomittauksia
+
+
+ Lisää ensimmäinen painomittaus
+
+
+ );
+
+ if (pets.length === 0) {
+ return (
+
+
+
+
+ Ei lemmikkejä
+
+
+ Lisää ensin lemmikki
+
+
+
+ );
+ }
+
+ const renderGraph = () => {
+ if (yearFilteredWeights.length === 0) return null;
+
+ // Group weights by month and calculate averages
+ const monthlyAverages = new Map();
+
+ yearFilteredWeights.forEach(record => {
+ const date = new Date(record.date);
+ const monthKey = `${date.getFullYear()}-${date.getMonth()}`; // e.g., "2025-0" for Jan 2025
+
+ if (!monthlyAverages.has(monthKey)) {
+ monthlyAverages.set(monthKey, { weights: [], dates: [] });
+ }
+
+ const monthData = monthlyAverages.get(monthKey)!;
+ monthData.weights.push(record.weight);
+ monthData.dates.push(record.date);
+ });
+
+ // Create averaged weight records (one per month)
+ const averagedWeights = Array.from(monthlyAverages.entries()).map(([monthKey, data]) => {
+ const [year, month] = monthKey.split('-').map(Number);
+ const avgWeight = data.weights.reduce((sum, w) => sum + w, 0) / data.weights.length;
+
+ return {
+ id: monthKey,
+ date: new Date(year, month, 15), // Use middle of month
+ weight: avgWeight,
+ };
+ }).sort((a, b) => a.date.getTime() - b.date.getTime());
+
+ // Get sorted weights (oldest to newest for graph)
+ const sortedWeights = averagedWeights;
+ const weights = sortedWeights.map(r => r.weight);
+ const maxWeight = Math.max(...weights);
+ const minWeight = Math.min(...weights);
+ const weightRange = maxWeight - minWeight || 1;
+ // Add 10% padding to top and bottom for better visibility
+ const paddedMax = maxWeight + (weightRange * 0.1);
+ const paddedMin = minWeight - (weightRange * 0.1);
+ const paddedRange = paddedMax - paddedMin;
+ const svgHeight = 220;
+ // Make graph slightly wider for better readability
+ const screenWidth = Dimensions.get('window').width;
+ const svgWidth = (screenWidth * 1.5) - 120; // 1.5x width for horizontal scrolling
+ // Add horizontal margins
+ const marginX = 20;
+
+ // Round y-axis labels to nice 5kg intervals
+ const roundToNice = (num: number, roundUp: boolean) => {
+ if (roundUp) {
+ return Math.ceil(num / 5) * 5;
+ } else {
+ return Math.floor(num / 5) * 5;
+ }
+ };
+
+ // Round the padded values to get nice labels at 5kg intervals
+ let maxLabel = roundToNice(paddedMax, true);
+ let minLabel = roundToNice(paddedMin, false);
+
+ // Ensure we have at least 10kg range for 3 labels (min, mid, max)
+ if (maxLabel - minLabel < 10) {
+ maxLabel = minLabel + 10;
+ }
+
+ // Calculate proper middle value - halfway between min and max
+ const midLabel = (minLabel + maxLabel) / 2;
+
+ // Setup time range for entire year (Jan 1 to Dec 31 of selected year)
+ const timeStart = new Date(selectedYear, 0, 1); // Jan 1
+ const timeEnd = new Date(selectedYear, 11, 31, 23, 59, 59); // Dec 31
+ const timeRange = timeEnd.getTime() - timeStart.getTime();
+
+ // Month labels - first letter of each Finnish month
+ const monthLabels = ['T', 'H', 'M', 'H', 'T', 'K', 'H', 'E', 'S', 'L', 'M', 'J'];
+
+ // Create x-axis labels for all 12 months
+ const xAxisLabels = monthLabels.map((label, idx) => {
+ // Position at middle of each month
+ const monthDate = new Date(selectedYear, idx, 15); // 15th of each month
+ return { date: monthDate, label };
+ });
+
+ return (
+
+
+ Painokehitys
+
+
+
+
+ {maxLabel}
+
+ kg
+ {midLabel}
+
+ {minLabel}
+
+
+
+
+
+
+
+ {xAxisLabels.map((label, idx) => {
+ const labelTime = label.date.getTime();
+ const xPos = marginX + ((labelTime - timeStart.getTime()) / timeRange) * (svgWidth - 2 * marginX);
+
+ return (
+
+ {label.label}
+
+ );
+ })}
+
+
+
+ pvm
+
+
+
+ {/* Year selection tabs */}
+ {availableYears.length > 1 && (
+
+
+ {availableYears.map((year) => (
+ setSelectedYear(year)}
+ showSelectedCheck={false}
+ style={[
+ styles.yearTab,
+ selectedYear === year && styles.selectedYearTab
+ ]}
+ textStyle={selectedYear === year ? styles.selectedYearTabText : styles.unselectedYearTabText}
+ >
+ {year}
+
+ ))}
+
+
+ )}
+
+ );
+ };
+
+ return (
+
+
+
+ {pets.map((pet) => (
+ setSelectedPetId(pet.id)}
+ style={[
+ styles.tab,
+ selectedPetId === pet.id && styles.selectedTab
+ ]}
+ textStyle={selectedPetId === pet.id ? styles.selectedTabText : styles.unselectedTabText}
+ icon={() => (
+
+ )}
+ >
+ {pet.name}
+
+ ))}
+
+
+
+
+ {selectedPetWeights.length > 0 && renderGraph()}
+
+
+
+
+ Mittaushistoria
+
+
+
+ {selectedPetWeights.length === 0 ? (
+ renderEmptyState()
+ ) : (
+ selectedPetWeights.map((record, index) => renderWeightCard(record, index))
+ )}
+
+
+ console.log('Lisää painomittaus')}
+ label="Lisää mittaus"
+ />
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.background,
+ },
+ tabsContainer: {
+ maxHeight: 70,
+ backgroundColor: COLORS.surface,
+ borderBottomWidth: 1,
+ borderBottomColor: COLORS.surfaceVariant,
+ },
+ tabsContent: {
+ paddingHorizontal: SPACING.md,
+ paddingVertical: SPACING.md,
+ gap: SPACING.sm,
+ alignItems: 'center',
+ justifyContent: 'center',
+ flexGrow: 1,
+ },
+ tab: {
+ marginHorizontal: SPACING.xs,
+ paddingHorizontal: SPACING.md,
+ height: 40,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+ selectedTab: {
+ backgroundColor: COLORS.primary,
+ elevation: 3,
+ },
+ selectedTabText: {
+ color: '#FFFFFF',
+ fontWeight: '700',
+ fontSize: 15,
+ lineHeight: 20,
+ },
+ unselectedTabText: {
+ fontSize: 15,
+ lineHeight: 20,
+ },
+ content: {
+ flex: 1,
+ },
+ scrollContent: {
+ paddingBottom: 80,
+ },
+ graphContainer: {
+ backgroundColor: COLORS.surface,
+ padding: SPACING.md,
+ paddingBottom: SPACING.xl,
+ marginHorizontal: SPACING.md,
+ marginTop: SPACING.md,
+ marginBottom: SPACING.md,
+ borderRadius: 12,
+ elevation: 2,
+ },
+ graphTitle: {
+ fontWeight: '600',
+ color: COLORS.onSurface,
+ marginBottom: SPACING.md,
+ },
+ graphContent: {
+ flexDirection: 'row',
+ },
+ yAxisContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ },
+ axisTitle: {
+ fontSize: 9,
+ color: COLORS.onSurfaceVariant,
+ fontWeight: '600',
+ },
+ yAxisLabels: {
+ width: 50,
+ height: 220,
+ justifyContent: 'space-between',
+ alignItems: 'flex-end',
+ paddingRight: SPACING.xs,
+ marginTop: -50,
+ },
+ yAxisMiddle: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 4,
+ },
+ axisLabel: {
+ color: COLORS.onSurfaceVariant,
+ textAlign: 'right',
+ lineHeight: 12,
+ },
+ graphArea: {
+ flex: 1,
+ position: 'relative',
+ },
+ gridLines: {
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ height: 220,
+ justifyContent: 'space-between',
+ },
+ gridLine: {
+ height: 1,
+ backgroundColor: COLORS.surfaceVariant,
+ opacity: 0.5,
+ },
+ svgGraph: {
+ marginBottom: SPACING.xs,
+ },
+ xAxisLabelsContainer: {
+ flexDirection: 'row',
+ justifyContent: 'space-around',
+ paddingBottom: SPACING.sm,
+ position: 'relative',
+ height: 20,
+ },
+ xAxisLabel: {
+ fontSize: 9,
+ color: COLORS.onSurfaceVariant,
+ textAlign: 'center',
+ flex: 1,
+ flexShrink: 1,
+ },
+ xAxisLabelShown: {
+ fontSize: 10,
+ color: COLORS.onSurfaceVariant,
+ textAlign: 'center',
+ flex: 1,
+ },
+ xAxisTitle: {
+ fontSize: 10,
+ color: COLORS.onSurfaceVariant,
+ fontWeight: '600',
+ textAlign: 'center',
+ marginTop: SPACING.xs,
+ },
+ yearTabsContainer: {
+ marginTop: SPACING.md,
+ paddingTop: SPACING.sm,
+ borderTopWidth: 1,
+ borderTopColor: COLORS.surfaceVariant,
+ },
+ yearTabsContent: {
+ gap: SPACING.xs,
+ paddingHorizontal: 4,
+ },
+ yearTab: {
+ backgroundColor: COLORS.surfaceVariant,
+ },
+ selectedYearTab: {
+ backgroundColor: COLORS.primary,
+ },
+ selectedYearTabText: {
+ color: '#FFFFFF',
+ fontWeight: '600',
+ },
+ unselectedYearTabText: {
+ color: COLORS.onSurfaceVariant,
+ },
+ dividerSection: {
+ paddingHorizontal: SPACING.md,
+ },
+ sectionTitle: {
+ fontWeight: '600',
+ color: COLORS.onSurface,
+ marginTop: SPACING.md,
+ marginBottom: SPACING.xs,
+ },
+ weightCard: {
+ marginHorizontal: SPACING.md,
+ marginBottom: SPACING.md,
+ elevation: 2,
+ },
+ weightHeader: {
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ marginBottom: SPACING.sm,
+ },
+ weightMainInfo: {
+ flexDirection: 'row',
+ alignItems: 'baseline',
+ gap: SPACING.xs,
+ },
+ weightValue: {
+ fontWeight: '700',
+ color: COLORS.primary,
+ },
+ weightUnit: {
+ color: COLORS.onSurfaceVariant,
+ fontWeight: '600',
+ },
+ changeChip: {
+ marginLeft: SPACING.sm,
+ },
+ increaseChip: {
+ backgroundColor: '#FFEBEE',
+ },
+ decreaseChip: {
+ backgroundColor: '#E8F5E9',
+ },
+ increaseChipText: {
+ color: '#D32F2F',
+ },
+ decreaseChipText: {
+ color: '#2E7D32',
+ },
+ dateContainer: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.xs,
+ marginTop: SPACING.xs,
+ },
+ dateText: {
+ color: COLORS.onSurfaceVariant,
+ },
+ divider: {
+ marginVertical: SPACING.sm,
+ },
+ weightDetail: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: SPACING.sm,
+ marginBottom: SPACING.xs,
+ },
+ detailText: {
+ flex: 1,
+ color: COLORS.onSurface,
+ },
+ notesContainer: {
+ flexDirection: 'row',
+ gap: SPACING.sm,
+ marginTop: SPACING.xs,
+ },
+ notesText: {
+ flex: 1,
+ color: COLORS.onSurfaceVariant,
+ fontStyle: 'italic',
+ },
+ emptyContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ paddingVertical: SPACING.xl * 2,
+ },
+ emptyTitle: {
+ marginTop: SPACING.md,
+ color: COLORS.onSurfaceVariant,
+ },
+ emptyText: {
+ marginTop: SPACING.xs,
+ color: COLORS.onSurfaceVariant,
+ textAlign: 'center',
+ },
+ fab: {
+ position: 'absolute',
+ right: SPACING.md,
+ bottom: SPACING.md,
+ backgroundColor: COLORS.primary,
+ },
+ graphScrollView: {
+ width: '100%',
+ },
+});
diff --git a/mobile/src/services/.gitkeep b/mobile/src/services/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/mobile/src/types/.gitkeep b/mobile/src/types/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/mobile/src/utils/.gitkeep b/mobile/src/utils/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/package-lock.json b/package-lock.json
index 38da06f..0a810e7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -32,6 +32,7 @@
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
+ "react-native-svg": "^15.15.1",
"react-native-worklets": "^0.5.1"
},
"devDependencies": {
@@ -5608,6 +5609,11 @@
"node": ">=0.6"
}
},
+ "node_modules/boolbase": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
+ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
+ },
"node_modules/bplist-creator": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
@@ -6148,6 +6154,52 @@
"node": ">=8"
}
},
+ "node_modules/css-select": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
+ "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
+ "dependencies": {
+ "boolbase": "^1.0.0",
+ "css-what": "^6.1.0",
+ "domhandler": "^5.0.2",
+ "domutils": "^3.0.1",
+ "nth-check": "^2.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
+ "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
+ "dependencies": {
+ "mdn-data": "2.0.14",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/css-tree/node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/css-what": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
+ "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
+ "engines": {
+ "node": ">= 6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/fb55"
+ }
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -6367,6 +6419,57 @@
"node": ">=8"
}
},
+ "node_modules/dom-serializer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
+ "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
+ "dependencies": {
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.2",
+ "entities": "^4.2.0"
+ },
+ "funding": {
+ "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
+ }
+ },
+ "node_modules/domelementtype": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+ "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/fb55"
+ }
+ ]
+ },
+ "node_modules/domhandler": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
+ "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
+ "dependencies": {
+ "domelementtype": "^2.3.0"
+ },
+ "engines": {
+ "node": ">= 4"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domhandler?sponsor=1"
+ }
+ },
+ "node_modules/domutils": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
+ "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
+ "dependencies": {
+ "dom-serializer": "^2.0.0",
+ "domelementtype": "^2.3.0",
+ "domhandler": "^5.0.3"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/domutils?sponsor=1"
+ }
+ },
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
@@ -6435,6 +6538,17 @@
"node": ">= 0.8"
}
},
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/env-editor": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz",
@@ -9050,6 +9164,11 @@
"node": ">= 0.4"
}
},
+ "node_modules/mdn-data": {
+ "version": "2.0.14",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
+ "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow=="
+ },
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@@ -9634,6 +9753,17 @@
"node": "^16.14.0 || >=18.0.0"
}
},
+ "node_modules/nth-check": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
+ "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
+ "dependencies": {
+ "boolbase": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/nth-check?sponsor=1"
+ }
+ },
"node_modules/nullthrows": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
@@ -10542,6 +10672,20 @@
"react-native": "*"
}
},
+ "node_modules/react-native-svg": {
+ "version": "15.15.1",
+ "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.1.tgz",
+ "integrity": "sha512-ZUD1xwc3Hwo4cOmOLumjJVoc7lEf9oQFlHnLmgccLC19fNm6LVEdtB+Cnip6gEi0PG3wfvVzskViEtrySQP8Fw==",
+ "dependencies": {
+ "css-select": "^5.1.0",
+ "css-tree": "^1.1.3",
+ "warn-once": "0.1.1"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/react-native-worklets": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",