v1.0.0 - First stable release
Stable version with: - WellNuo mobile app (React Native + Expo) - Beneficiaries management - Dashboard integration - API documentation in wellnuoSheme/ - App icons and assets - EAS build configuration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
15
app.json
@ -9,7 +9,11 @@
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.wellnuo.app",
|
||||
"infoPlist": {
|
||||
"ITSAppUsesNonExemptEncryption": false
|
||||
}
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
@ -43,6 +47,13 @@
|
||||
"experiments": {
|
||||
"typedRoutes": true,
|
||||
"reactCompiler": true
|
||||
}
|
||||
},
|
||||
"extra": {
|
||||
"router": {},
|
||||
"eas": {
|
||||
"projectId": "4a77e46d-7b0e-4ace-a385-006b07027234"
|
||||
}
|
||||
},
|
||||
"owner": "kosyakorel1"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Tabs } from 'expo-router';
|
||||
import React from 'react';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { Feather } from '@expo/vector-icons';
|
||||
|
||||
import { HapticTab } from '@/components/haptic-tab';
|
||||
import { AppColors } from '@/constants/theme';
|
||||
@ -18,6 +18,13 @@ export default function TabLayout() {
|
||||
tabBarStyle: {
|
||||
backgroundColor: isDark ? '#151718' : AppColors.background,
|
||||
borderTopColor: isDark ? '#2D3135' : AppColors.border,
|
||||
height: 85,
|
||||
paddingBottom: 25,
|
||||
paddingTop: 10,
|
||||
},
|
||||
tabBarLabelStyle: {
|
||||
fontSize: 11,
|
||||
fontWeight: '500',
|
||||
},
|
||||
headerShown: false,
|
||||
tabBarButton: HapticTab,
|
||||
@ -26,13 +33,13 @@ export default function TabLayout() {
|
||||
<Tabs.Screen
|
||||
name="index"
|
||||
options={{
|
||||
title: 'Home',
|
||||
title: 'Dashboard',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="home" size={size} color={color} />
|
||||
<Feather name="grid" size={22} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
{/* Hide dashboard - now accessed via beneficiary selection */}
|
||||
{/* Hide old dashboard - now index shows WebView dashboard */}
|
||||
<Tabs.Screen
|
||||
name="dashboard"
|
||||
options={{
|
||||
@ -44,7 +51,7 @@ export default function TabLayout() {
|
||||
options={{
|
||||
title: 'Chat',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="chatbubble-ellipses" size={size} color={color} />
|
||||
<Feather name="message-circle" size={22} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@ -53,7 +60,7 @@ export default function TabLayout() {
|
||||
options={{
|
||||
title: 'Profile',
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="person" size={size} color={color} />
|
||||
<Feather name="user" size={22} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
@ -64,6 +71,13 @@ export default function TabLayout() {
|
||||
href: null,
|
||||
}}
|
||||
/>
|
||||
{/* Beneficiaries - hidden from tab bar but keeps tab bar visible */}
|
||||
<Tabs.Screen
|
||||
name="beneficiaries"
|
||||
options={{
|
||||
href: null,
|
||||
}}
|
||||
/>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,10 +4,13 @@ import { WebView } from 'react-native-webview';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useLocalSearchParams, router } from 'expo-router';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
||||
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
||||
|
||||
// Start with login page, then redirect to dashboard after auth
|
||||
const LOGIN_URL = 'https://react.eluxnetworks.net/login';
|
||||
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
|
||||
|
||||
export default function BeneficiaryDashboardScreen() {
|
||||
@ -17,9 +20,56 @@ export default function BeneficiaryDashboardScreen() {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [canGoBack, setCanGoBack] = useState(false);
|
||||
const [authToken, setAuthToken] = useState<string | null>(null);
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
|
||||
const [webViewUrl, setWebViewUrl] = useState(DASHBOARD_URL);
|
||||
|
||||
const beneficiaryName = currentBeneficiary?.name || 'Dashboard';
|
||||
|
||||
// Load token, username, and userId from SecureStore
|
||||
useEffect(() => {
|
||||
const loadCredentials = async () => {
|
||||
try {
|
||||
const token = await SecureStore.getItemAsync('accessToken');
|
||||
const user = await SecureStore.getItemAsync('userName');
|
||||
const uid = await SecureStore.getItemAsync('userId');
|
||||
setAuthToken(token);
|
||||
setUserName(user);
|
||||
setUserId(uid);
|
||||
console.log('Loaded credentials for WebView:', { hasToken: !!token, user, uid });
|
||||
} catch (err) {
|
||||
console.error('Failed to load credentials:', err);
|
||||
} finally {
|
||||
setIsTokenLoaded(true);
|
||||
}
|
||||
};
|
||||
loadCredentials();
|
||||
}, []);
|
||||
|
||||
// JavaScript to inject token into localStorage before page loads
|
||||
// Web app uses auth2 key with JSON object: {username, token, user_id}
|
||||
const injectedJavaScript = authToken
|
||||
? `
|
||||
(function() {
|
||||
try {
|
||||
// Web app expects auth2 as JSON object with these exact fields
|
||||
var authData = {
|
||||
username: '${userName || ''}',
|
||||
token: '${authToken}',
|
||||
user_id: ${userId || 'null'}
|
||||
};
|
||||
localStorage.setItem('auth2', JSON.stringify(authData));
|
||||
console.log('Auth data injected:', authData.username, 'user_id:', authData.user_id);
|
||||
} catch(e) {
|
||||
console.error('Failed to inject token:', e);
|
||||
}
|
||||
})();
|
||||
true;
|
||||
`
|
||||
: '';
|
||||
|
||||
const handleRefresh = () => {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
@ -45,6 +95,25 @@ export default function BeneficiaryDashboardScreen() {
|
||||
router.back();
|
||||
};
|
||||
|
||||
// Wait for token to load before showing WebView
|
||||
if (!isTokenLoaded) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity style={styles.backButton} onPress={handleGoBack}>
|
||||
<Ionicons name="arrow-back" size={24} color={AppColors.textPrimary} />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>{beneficiaryName}</Text>
|
||||
<View style={styles.placeholder} />
|
||||
</View>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
<Text style={styles.loadingText}>Preparing dashboard...</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
@ -100,7 +169,7 @@ export default function BeneficiaryDashboardScreen() {
|
||||
<View style={styles.webViewContainer}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ uri: DASHBOARD_URL }}
|
||||
source={{ uri: webViewUrl }}
|
||||
style={styles.webView}
|
||||
onLoadStart={() => setIsLoading(true)}
|
||||
onLoadEnd={() => setIsLoading(false)}
|
||||
@ -112,6 +181,10 @@ export default function BeneficiaryDashboardScreen() {
|
||||
startInLoadingState={true}
|
||||
scalesPageToFit={true}
|
||||
allowsBackForwardNavigationGestures={true}
|
||||
// Inject token into localStorage BEFORE content loads
|
||||
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
||||
// Also inject after load in case page reads localStorage late
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
renderLoading={() => (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
@ -144,7 +217,7 @@ export default function BeneficiaryDashboardScreen() {
|
||||
|
||||
<TouchableOpacity
|
||||
style={styles.quickAction}
|
||||
onPress={() => router.push(`/beneficiaries/${id}`)}
|
||||
onPress={() => router.push(`./`)}
|
||||
>
|
||||
<Ionicons name="person" size={24} color={AppColors.primary} />
|
||||
<Text style={styles.quickActionText}>Details</Text>
|
||||
@ -1,143 +1,127 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
RefreshControl,
|
||||
} from 'react-native';
|
||||
import { router } from 'expo-router';
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { View, Text, StyleSheet, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||
import { WebView } from 'react-native-webview';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { api } from '@/services/api';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useBeneficiary } from '@/contexts/BeneficiaryContext';
|
||||
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
|
||||
import { FullScreenError } from '@/components/ui/ErrorMessage';
|
||||
import { AppColors, BorderRadius, FontSizes, Spacing } from '@/constants/theme';
|
||||
import type { Beneficiary } from '@/types';
|
||||
import { AppColors, FontSizes, Spacing } from '@/constants/theme';
|
||||
|
||||
export default function BeneficiariesListScreen() {
|
||||
const DASHBOARD_URL = 'https://react.eluxnetworks.net/dashboard';
|
||||
|
||||
export default function HomeScreen() {
|
||||
const { user } = useAuth();
|
||||
const { setCurrentBeneficiary } = useBeneficiary();
|
||||
const [beneficiaries, setBeneficiaries] = useState<Beneficiary[]>([]);
|
||||
const webViewRef = useRef<WebView>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [canGoBack, setCanGoBack] = useState(false);
|
||||
const [authToken, setAuthToken] = useState<string | null>(null);
|
||||
const [userName, setUserName] = useState<string | null>(null);
|
||||
const [userId, setUserId] = useState<string | null>(null);
|
||||
const [isTokenLoaded, setIsTokenLoaded] = useState(false);
|
||||
|
||||
const loadBeneficiaries = useCallback(async (showLoading = true) => {
|
||||
if (showLoading) setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await api.getBeneficiaries();
|
||||
|
||||
if (response.ok && response.data) {
|
||||
setBeneficiaries(response.data.beneficiaries);
|
||||
} else {
|
||||
setError(response.error?.message || 'Failed to load beneficiaries');
|
||||
// Load credentials from SecureStore
|
||||
useEffect(() => {
|
||||
const loadCredentials = async () => {
|
||||
try {
|
||||
const token = await SecureStore.getItemAsync('accessToken');
|
||||
const user = await SecureStore.getItemAsync('userName');
|
||||
const uid = await SecureStore.getItemAsync('userId');
|
||||
setAuthToken(token);
|
||||
setUserName(user);
|
||||
setUserId(uid);
|
||||
console.log('Home: Loaded credentials for WebView:', { hasToken: !!token, user, uid });
|
||||
} catch (err) {
|
||||
console.error('Failed to load credentials:', err);
|
||||
} finally {
|
||||
setIsTokenLoaded(true);
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'An error occurred');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
loadCredentials();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadBeneficiaries();
|
||||
}, [loadBeneficiaries]);
|
||||
// JavaScript to inject auth token into localStorage
|
||||
// Web app expects auth2 as JSON: {username, token, user_id}
|
||||
const injectedJavaScript = authToken
|
||||
? `
|
||||
(function() {
|
||||
try {
|
||||
var authData = {
|
||||
username: '${userName || ''}',
|
||||
token: '${authToken}',
|
||||
user_id: ${userId || 'null'}
|
||||
};
|
||||
localStorage.setItem('auth2', JSON.stringify(authData));
|
||||
console.log('Auth injected:', authData.username);
|
||||
} catch(e) {
|
||||
console.error('Failed to inject token:', e);
|
||||
}
|
||||
})();
|
||||
true;
|
||||
`
|
||||
: '';
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
setIsRefreshing(true);
|
||||
loadBeneficiaries(false);
|
||||
}, [loadBeneficiaries]);
|
||||
|
||||
const handleBeneficiaryPress = (beneficiary: Beneficiary) => {
|
||||
// Set current beneficiary in context before navigating
|
||||
setCurrentBeneficiary(beneficiary);
|
||||
// Navigate directly to their dashboard
|
||||
router.push(`/beneficiaries/${beneficiary.id}/dashboard`);
|
||||
const handleRefresh = () => {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
webViewRef.current?.reload();
|
||||
};
|
||||
|
||||
const renderBeneficiaryCard = ({ item }: { item: Beneficiary }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.beneficiaryCard}
|
||||
onPress={() => handleBeneficiaryPress(item)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<View style={styles.beneficiaryInfo}>
|
||||
<View style={styles.avatarContainer}>
|
||||
<Text style={styles.avatarText}>
|
||||
{item.name.charAt(0).toUpperCase()}
|
||||
</Text>
|
||||
<View
|
||||
style={[
|
||||
styles.statusIndicator,
|
||||
item.status === 'online' ? styles.online : styles.offline,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
const handleWebViewBack = () => {
|
||||
if (canGoBack) {
|
||||
webViewRef.current?.goBack();
|
||||
}
|
||||
};
|
||||
|
||||
<View style={styles.beneficiaryDetails}>
|
||||
<Text style={styles.beneficiaryName}>{item.name}</Text>
|
||||
<Text style={styles.beneficiaryRelationship}>{item.relationship}</Text>
|
||||
<Text style={styles.lastActivity}>
|
||||
<Ionicons name="time-outline" size={12} color={AppColors.textMuted} />{' '}
|
||||
{item.last_activity}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
const handleNavigationStateChange = (navState: any) => {
|
||||
setCanGoBack(navState.canGoBack);
|
||||
};
|
||||
|
||||
{item.sensor_data && (
|
||||
<View style={styles.sensorStats}>
|
||||
<View style={styles.statItem}>
|
||||
<Ionicons
|
||||
name={item.sensor_data.motion_detected ? "walk" : "walk-outline"}
|
||||
size={16}
|
||||
color={item.sensor_data.motion_detected ? AppColors.online : AppColors.textMuted}
|
||||
/>
|
||||
<Text style={styles.statValue}>
|
||||
{item.sensor_data.motion_detected ? 'Active' : 'Inactive'}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>Motion</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Ionicons
|
||||
name={item.sensor_data.door_status === 'open' ? "enter-outline" : "home-outline"}
|
||||
size={16}
|
||||
color={item.sensor_data.door_status === 'open' ? AppColors.warning : AppColors.primary}
|
||||
/>
|
||||
<Text style={styles.statValue}>
|
||||
{item.sensor_data.door_status === 'open' ? 'Open' : 'Closed'}
|
||||
</Text>
|
||||
<Text style={styles.statLabel}>Door</Text>
|
||||
</View>
|
||||
<View style={styles.statItem}>
|
||||
<Ionicons name="thermometer-outline" size={16} color={AppColors.primaryDark} />
|
||||
<Text style={styles.statValue}>{item.sensor_data.temperature}°</Text>
|
||||
<Text style={styles.statLabel}>Temp</Text>
|
||||
const handleError = () => {
|
||||
setError('Failed to load dashboard. Please check your internet connection.');
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
// Wait for token to load
|
||||
if (!isTokenLoaded) {
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
|
||||
<Text style={styles.headerTitle}>Dashboard</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={20}
|
||||
color={AppColors.textMuted}
|
||||
style={styles.chevron}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner fullScreen message="Loading beneficiaries..." />;
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
<Text style={styles.loadingText}>Preparing dashboard...</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <FullScreenError message={error} onRetry={() => loadBeneficiaries()} />;
|
||||
return (
|
||||
<SafeAreaView style={styles.container} edges={['top']}>
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
|
||||
<Text style={styles.headerTitle}>Dashboard</Text>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.refreshButton} onPress={handleRefresh}>
|
||||
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.errorContainer}>
|
||||
<Ionicons name="cloud-offline-outline" size={64} color={AppColors.textMuted} />
|
||||
<Text style={styles.errorTitle}>Connection Error</Text>
|
||||
<Text style={styles.errorText}>{error}</Text>
|
||||
<TouchableOpacity style={styles.retryButton} onPress={handleRefresh}>
|
||||
<Text style={styles.retryButtonText}>Try Again</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -145,40 +129,53 @@ export default function BeneficiariesListScreen() {
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<View>
|
||||
<Text style={styles.greeting}>
|
||||
Hello, {user?.user_name || 'User'}
|
||||
</Text>
|
||||
<Text style={styles.headerTitle}>Beneficiaries</Text>
|
||||
<Text style={styles.greeting}>Hello, {user?.user_name || 'User'}</Text>
|
||||
<Text style={styles.headerTitle}>Dashboard</Text>
|
||||
</View>
|
||||
<View style={styles.headerActions}>
|
||||
{canGoBack && (
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleWebViewBack}>
|
||||
<Ionicons name="chevron-back" size={22} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity style={styles.actionButton} onPress={handleRefresh}>
|
||||
<Ionicons name="refresh" size={22} color={AppColors.primary} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<TouchableOpacity style={styles.addButton}>
|
||||
<Ionicons name="add" size={24} color={AppColors.white} />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Beneficiary List */}
|
||||
<FlatList
|
||||
data={beneficiaries}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={renderBeneficiaryCard}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefreshing}
|
||||
onRefresh={handleRefresh}
|
||||
tintColor={AppColors.primary}
|
||||
/>
|
||||
}
|
||||
ListEmptyComponent={
|
||||
<View style={styles.emptyContainer}>
|
||||
<Ionicons name="people-outline" size={64} color={AppColors.textMuted} />
|
||||
<Text style={styles.emptyTitle}>No beneficiaries yet</Text>
|
||||
<Text style={styles.emptyText}>
|
||||
Add your first beneficiary to start monitoring
|
||||
</Text>
|
||||
{/* WebView Dashboard */}
|
||||
<View style={styles.webViewContainer}>
|
||||
<WebView
|
||||
ref={webViewRef}
|
||||
source={{ uri: DASHBOARD_URL }}
|
||||
style={styles.webView}
|
||||
onLoadStart={() => setIsLoading(true)}
|
||||
onLoadEnd={() => setIsLoading(false)}
|
||||
onError={handleError}
|
||||
onHttpError={handleError}
|
||||
onNavigationStateChange={handleNavigationStateChange}
|
||||
javaScriptEnabled={true}
|
||||
domStorageEnabled={true}
|
||||
startInLoadingState={true}
|
||||
scalesPageToFit={true}
|
||||
allowsBackForwardNavigationGestures={true}
|
||||
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
||||
injectedJavaScript={injectedJavaScript}
|
||||
renderLoading={() => (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
<Text style={styles.loadingText}>Loading dashboard...</Text>
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
|
||||
{isLoading && (
|
||||
<View style={styles.loadingOverlay}>
|
||||
<ActivityIndicator size="large" color={AppColors.primary} />
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
@ -186,7 +183,7 @@ export default function BeneficiariesListScreen() {
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: AppColors.surface,
|
||||
backgroundColor: AppColors.background,
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
@ -207,122 +204,76 @@ const styles = StyleSheet.create({
|
||||
fontWeight: '700',
|
||||
color: AppColors.textPrimary,
|
||||
},
|
||||
addButton: {
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: BorderRadius.full,
|
||||
backgroundColor: AppColors.primary,
|
||||
justifyContent: 'center',
|
||||
headerActions: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
listContent: {
|
||||
padding: Spacing.md,
|
||||
actionButton: {
|
||||
padding: Spacing.xs,
|
||||
marginLeft: Spacing.xs,
|
||||
},
|
||||
beneficiaryCard: {
|
||||
refreshButton: {
|
||||
padding: Spacing.xs,
|
||||
},
|
||||
webViewContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
webView: {
|
||||
flex: 1,
|
||||
},
|
||||
loadingContainer: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: AppColors.background,
|
||||
borderRadius: BorderRadius.lg,
|
||||
padding: Spacing.md,
|
||||
marginBottom: Spacing.md,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.05,
|
||||
shadowRadius: 4,
|
||||
elevation: 2,
|
||||
},
|
||||
beneficiaryInfo: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: Spacing.md,
|
||||
},
|
||||
avatarContainer: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: BorderRadius.full,
|
||||
backgroundColor: AppColors.primaryLight,
|
||||
loadingOverlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginRight: Spacing.md,
|
||||
backgroundColor: 'rgba(255,255,255,0.8)',
|
||||
},
|
||||
avatarText: {
|
||||
fontSize: FontSizes.xl,
|
||||
fontWeight: '600',
|
||||
color: AppColors.white,
|
||||
},
|
||||
statusIndicator: {
|
||||
position: 'absolute',
|
||||
bottom: 2,
|
||||
right: 2,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: BorderRadius.full,
|
||||
borderWidth: 2,
|
||||
borderColor: AppColors.background,
|
||||
},
|
||||
online: {
|
||||
backgroundColor: AppColors.online,
|
||||
},
|
||||
offline: {
|
||||
backgroundColor: AppColors.offline,
|
||||
},
|
||||
beneficiaryDetails: {
|
||||
flex: 1,
|
||||
},
|
||||
beneficiaryName: {
|
||||
fontSize: FontSizes.lg,
|
||||
fontWeight: '600',
|
||||
color: AppColors.textPrimary,
|
||||
},
|
||||
beneficiaryRelationship: {
|
||||
fontSize: FontSizes.sm,
|
||||
color: AppColors.textSecondary,
|
||||
marginTop: 2,
|
||||
},
|
||||
lastActivity: {
|
||||
fontSize: FontSizes.xs,
|
||||
color: AppColors.textMuted,
|
||||
marginTop: 4,
|
||||
},
|
||||
sensorStats: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-around',
|
||||
paddingTop: Spacing.md,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: AppColors.border,
|
||||
},
|
||||
statItem: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
statValue: {
|
||||
loadingText: {
|
||||
marginTop: Spacing.md,
|
||||
fontSize: FontSizes.base,
|
||||
fontWeight: '600',
|
||||
color: AppColors.textPrimary,
|
||||
marginTop: Spacing.xs,
|
||||
color: AppColors.textSecondary,
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: FontSizes.xs,
|
||||
color: AppColors.textMuted,
|
||||
},
|
||||
chevron: {
|
||||
position: 'absolute',
|
||||
top: Spacing.md,
|
||||
right: Spacing.md,
|
||||
},
|
||||
emptyContainer: {
|
||||
errorContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
paddingTop: Spacing.xxl * 2,
|
||||
padding: Spacing.xl,
|
||||
},
|
||||
emptyTitle: {
|
||||
errorTitle: {
|
||||
fontSize: FontSizes.lg,
|
||||
fontWeight: '600',
|
||||
color: AppColors.textPrimary,
|
||||
marginTop: Spacing.md,
|
||||
},
|
||||
emptyText: {
|
||||
errorText: {
|
||||
fontSize: FontSizes.base,
|
||||
color: AppColors.textSecondary,
|
||||
textAlign: 'center',
|
||||
marginTop: Spacing.xs,
|
||||
},
|
||||
retryButton: {
|
||||
marginTop: Spacing.lg,
|
||||
paddingHorizontal: Spacing.xl,
|
||||
paddingVertical: Spacing.md,
|
||||
backgroundColor: AppColors.primary,
|
||||
borderRadius: 8,
|
||||
},
|
||||
retryButtonText: {
|
||||
color: AppColors.white,
|
||||
fontSize: FontSizes.base,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
@ -44,7 +44,6 @@ function RootLayoutNav() {
|
||||
<Stack screenOptions={{ headerShown: false }}>
|
||||
<Stack.Screen name="(auth)" />
|
||||
<Stack.Screen name="(tabs)" />
|
||||
<Stack.Screen name="beneficiaries" />
|
||||
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
|
||||
</Stack>
|
||||
<StatusBar style="auto" />
|
||||
|
||||
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 384 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 35 KiB |
26
eas.json
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"cli": {
|
||||
"version": ">= 5.0.0",
|
||||
"appVersionSource": "remote"
|
||||
},
|
||||
"build": {
|
||||
"development": {
|
||||
"developmentClient": true,
|
||||
"distribution": "internal"
|
||||
},
|
||||
"preview": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {
|
||||
"autoIncrement": true
|
||||
}
|
||||
},
|
||||
"submit": {
|
||||
"production": {
|
||||
"ios": {
|
||||
"appleId": "serter2069@gmail.com",
|
||||
"ascAppId": "WILL_BE_SET_AFTER_FIRST_BUILD"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
549
package-lock.json
generated
@ -41,6 +41,7 @@
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"playwright": "^1.57.0",
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "~5.9.2"
|
||||
}
|
||||
},
|
||||
@ -2275,6 +2276,496 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/colour": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
|
||||
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
|
||||
"integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
|
||||
"integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-riscv64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
|
||||
"integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
|
||||
"integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
|
||||
"integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
|
||||
"integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
|
||||
"integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
|
||||
"integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-riscv64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
|
||||
"integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
|
||||
"integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
|
||||
"integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.7.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
|
||||
"integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
|
||||
"integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
|
||||
"integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
@ -11126,6 +11617,64 @@
|
||||
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.34.5",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
|
||||
"integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@img/colour": "^1.0.0",
|
||||
"detect-libc": "^2.1.2",
|
||||
"semver": "^7.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.5",
|
||||
"@img/sharp-darwin-x64": "0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64": "1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.4",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.4",
|
||||
"@img/sharp-linux-arm": "0.34.5",
|
||||
"@img/sharp-linux-arm64": "0.34.5",
|
||||
"@img/sharp-linux-ppc64": "0.34.5",
|
||||
"@img/sharp-linux-riscv64": "0.34.5",
|
||||
"@img/sharp-linux-s390x": "0.34.5",
|
||||
"@img/sharp-linux-x64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.5",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.5",
|
||||
"@img/sharp-wasm32": "0.34.5",
|
||||
"@img/sharp-win32-arm64": "0.34.5",
|
||||
"@img/sharp-win32-ia32": "0.34.5",
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@ -44,6 +44,7 @@
|
||||
"eslint": "^9.25.0",
|
||||
"eslint-config-expo": "~10.0.0",
|
||||
"playwright": "^1.57.0",
|
||||
"sharp": "^0.34.5",
|
||||
"typescript": "~5.9.2"
|
||||
},
|
||||
"private": true
|
||||
|
||||