From f919beaac0c4510627d231e92f58564c86a2e8a8 Mon Sep 17 00:00:00 2001 From: Ethan Burnet Date: Sun, 10 Aug 2025 11:12:21 +1000 Subject: [PATCH] Refactored React code for separation of concerns --- pawsense/App.js | 425 ++++------------------------- pawsense/AppStyles.js | 6 +- pawsense/components/Behavior.jsx | 62 +++++ pawsense/components/Dashboard.jsx | 61 +++++ pawsense/components/Health.jsx | 63 +++++ pawsense/components/Location.jsx | 55 ++++ pawsense/components/Translator.jsx | 70 +++++ pawsense/components/components.jsx | 40 +++ 8 files changed, 398 insertions(+), 384 deletions(-) create mode 100644 pawsense/components/Behavior.jsx create mode 100644 pawsense/components/Dashboard.jsx create mode 100644 pawsense/components/Health.jsx create mode 100644 pawsense/components/Location.jsx create mode 100644 pawsense/components/Translator.jsx create mode 100644 pawsense/components/components.jsx diff --git a/pawsense/App.js b/pawsense/App.js index 0a0ca20..bc8395f 100644 --- a/pawsense/App.js +++ b/pawsense/App.js @@ -1,22 +1,21 @@ -// App.js -import React, { useState, useEffect } from 'react'; +import { useState, useEffect } from 'react'; import { - StyleSheet, Text, View, - ScrollView, - TouchableOpacity, - TextInput, SafeAreaView, StatusBar, - Dimensions, Alert } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { LinearGradient } from 'expo-linear-gradient'; -import styles from './AppStyles'; -const { width } = Dimensions.get('window'); +import styles from './AppStyles.js'; +import { TabButton } from './components/components.jsx'; +import Dashboard from "./components/Dashboard.jsx"; +import Health from "./components/Health.jsx"; +import Location from "./components/Location.jsx"; +import Behavior from "./components/Behavior.jsx"; +import Translator from "./components/Translator.jsx"; // Mock data generators const generateHealthData = () => ({ @@ -35,44 +34,6 @@ const locations = [ { name: 'Vet Clinic', lat: 40.7505, lng: -73.9934 } ]; -// Reusable Components -const Card = ({ children, style }) => ( - - {children} - -); - -const Badge = ({ text, style, textStyle }) => ( - - {text} - -); - -const ProgressBar = ({ progress, color = '#3B82F6', height = 8 }) => ( - - - -); - -const TabButton = ({ icon, isActive, onPress, label }) => ( - - - {label && {label}} - -); - export default function SmartDogCollarApp() { const [currentTab, setCurrentTab] = useState('dashboard'); const [healthData, setHealthData] = useState(generateHealthData()); @@ -103,14 +64,14 @@ export default function SmartDogCollarApp() { return () => clearInterval(interval); }, []); - // + // const handleTranslate = () => { if (!translatorInput.trim()) { Alert.alert('Please enter a message'); return; } - + const responses = [ 'Woof woof! (Time for a walk!)', 'Bark bark woof! (I love you too!)', @@ -132,7 +93,7 @@ export default function SmartDogCollarApp() { return emojiMap[behavior] || '🐕'; }; - + useEffect(() => { async function fetchPrediction() { try { @@ -170,7 +131,7 @@ export default function SmartDogCollarApp() { console.error('Error fetching prediction:', error); } } - + fetchPrediction(); }, []); @@ -193,326 +154,17 @@ export default function SmartDogCollarApp() { console.error('Error fetching client data:', error); } } - + fetchClientData(); }, []); // console.log('Predictions: ', predictions) // console.log('Predictions: ', clientData) - const renderDashboard = () => ( - - - Current Status - - - {clientData.heart_rate} - BPM - - - {clientData.temperature}°F - Temperature - - - - Activity Level - - - - - - - Behavior & Emotion - - - {clientData.activity} - Current Activity - - - - - Last updated: {new Date().toLocaleTimeString()} - - - - - Location - - - {clientData.context} - - {clientData.latitude}, {clientData.longitude} - - - - - - - ); - - const renderHealth = () => ( - - - Vital Signs - - - - - Heart Rate - - {clientData.heart_rate} - BPM (Normal) - - - - - Temperature - - {clientData.temperature}°F - Normal Range - - - - - - Daily Activity - - - Steps Today - {clientData.steps} - - - Goal: 15,000 steps - - - - Calories Burned - {clientData.calories} - - - Goal: 600 calories - - - - - Health Alerts - - - ⚠️ Health Alert! - - - Max has missed his medicine dose today. - - - - - ); - - const renderLocation = () => ( - - - Live Location - - - {clientData.context} - - {clientData.latitude}, {clientData.longitude} - - - - - - Distance from Home - 0.3 miles - - - Last Movement - 2 minutes ago - - - - - - Recent Locations - {locations.map((location, index) => ( - - - - - {location.name} - - {Math.floor(Math.random() * 60)} min ago - - - - - - ))} - - - ); - - const renderBehavior = () => ( - - - Current Behavior - - {getBehaviorEmoji(currentBehavior)} - {currentBehavior} - Confidence: 94% - - - - - - - Activity Predictions - - {predictions.length > 0 ? ( - [...predictions] // create a shallow copy to avoid mutating state directly - .sort((a, b) => { - // extract minutes from timeInfo like "in 123 minutes" - const aMinutes = parseInt(a.timeInfo.match(/in (\d+) minutes/)[1], 10); - const bMinutes = parseInt(b.timeInfo.match(/in (\d+) minutes/)[1], 10); - return aMinutes - bMinutes; - }) - .map((pred, idx) => { - const [hour, minute] = pred.time.split(':'); - - const minutesMatch = pred.timeInfo.match(/in (\d+) minutes/); - let timeInfoFormatted = pred.timeInfo; - if (minutesMatch) { - const totalMinutes = parseInt(minutesMatch[1], 10); - if (totalMinutes >= 60) { - const hrs = Math.floor(totalMinutes / 60); - const mins = totalMinutes % 60; - timeInfoFormatted = `in ${hrs} hour${hrs > 1 ? 's' : ''} ${mins} minute${mins !== 1 ? 's' : ''}`; - } - } - - return ( - - {pred.label} - - - ); - }) - ) : ( - Loading predictions... - )} - - - ); - - const renderTranslator = () => ( - - - Two-Way Translator - - Speak to Max: - - - - - - - - {translatorOutput && ( - - Max's Response: - {translatorOutput} - - )} - - - - Recent Bark Translations - {recentBarks.map((bark, index) => ( - - - {bark.translation} - - - {bark.time} - - ))} - - - - Quick Commands - - {['Sit', 'Stay', 'Come', 'Good Boy'].map((command) => ( - { - setTranslatorInput(command); - handleTranslate(); - }} - > - {command} - - ))} - - - - ); - - const renderTabContent = () => { - switch (currentTab) { - case 'dashboard': - return renderDashboard(); - case 'health': - return renderHealth(); - case 'location': - return renderLocation(); - case 'behavior': - return renderBehavior(); - case 'translator': - return renderTranslator(); - default: - return renderDashboard(); - } - }; - return ( - + {/* Header */} - {Math.floor(batteryLevel)}% @@ -546,35 +198,50 @@ export default function SmartDogCollarApp() { {/* Tab Navigation */} - setCurrentTab('dashboard')} /> - setCurrentTab('health')} /> - setCurrentTab('location')} /> - setCurrentTab('behavior')} /> - setCurrentTab('translator')} /> - {/* Tab Content */} - {renderTabContent()} + {(() => { + const DashboardBound = Dashboard.bind(this, healthData, clientData, currentEmotion) + switch (currentTab) { + case 'dashboard': + return DashboardBound(); + case 'health': + return Health(clientData); + case 'location': + return Location(clientData, locations); + case 'behavior': + return Behavior(getBehaviorEmoji, currentBehavior, currentEmotion, predictions); + case 'translator': + return Translator(translatorInput, setTranslatorInput, handleTranslate, translatorOutput, recentBarks); + default: + return DashboardBound(); + } + })()} ); -} \ No newline at end of file +} diff --git a/pawsense/AppStyles.js b/pawsense/AppStyles.js index a120117..5549268 100644 --- a/pawsense/AppStyles.js +++ b/pawsense/AppStyles.js @@ -1,9 +1,5 @@ -// styles.js import { StyleSheet, Dimensions } from 'react-native'; -const { width, height } = Dimensions.get('window'); - - const styles = StyleSheet.create({ container: { flex: 1, @@ -497,7 +493,7 @@ const styles = StyleSheet.create({ borderRadius: 8, paddingHorizontal: 16, paddingVertical: 8, - minWidth: (width - 64) / 2 - 4, + minWidth: (Dimensions.get('window').width - 64) / 2 - 4, alignItems: 'center', }, commandButtonText: { diff --git a/pawsense/components/Behavior.jsx b/pawsense/components/Behavior.jsx new file mode 100644 index 0000000..52abc9c --- /dev/null +++ b/pawsense/components/Behavior.jsx @@ -0,0 +1,62 @@ +import { ScrollView, Text, View } from "react-native"; +import { Card, Badge } from "./components"; +import styles from "../AppStyles"; + +export default (getBehaviorEmoji, currentBehavior, currentEmotion, predictions) => ( + + + Current Behavior + + {getBehaviorEmoji(currentBehavior)} + {currentBehavior} + Confidence: 94% + + + + + + + Activity Predictions + + {predictions.length > 0 ? ( + [...predictions] // create a shallow copy to avoid mutating state directly + .sort((a, b) => { + // extract minutes from timeInfo like "in 123 minutes" + const aMinutes = parseInt(a.timeInfo.match(/in (\d+) minutes/)[1], 10); + const bMinutes = parseInt(b.timeInfo.match(/in (\d+) minutes/)[1], 10); + return aMinutes - bMinutes; + }) + .map((pred, idx) => { + const [hour, minute] = pred.time.split(':'); + + const minutesMatch = pred.timeInfo.match(/in (\d+) minutes/); + let timeInfoFormatted = pred.timeInfo; + if (minutesMatch) { + const totalMinutes = parseInt(minutesMatch[1], 10); + if (totalMinutes >= 60) { + const hrs = Math.floor(totalMinutes / 60); + const mins = totalMinutes % 60; + timeInfoFormatted = `in ${hrs} hour${hrs > 1 ? 's' : ''} ${mins} minute${mins !== 1 ? 's' : ''}`; + } + } + + return ( + + {pred.label} + + + ); + }) + ) : ( + Loading predictions... + )} + + +); diff --git a/pawsense/components/Dashboard.jsx b/pawsense/components/Dashboard.jsx new file mode 100644 index 0000000..543f34f --- /dev/null +++ b/pawsense/components/Dashboard.jsx @@ -0,0 +1,61 @@ +import { ScrollView, Text, View } from "react-native"; +import styles from "../AppStyles.js"; +import { Card, Badge, ProgressBar } from "./components.jsx"; + +export default (healthData, clientData, currentEmotion) => ( + + + Current Status + + + {clientData.heart_rate} + BPM + + + {clientData.temperature}°F + Temperature + + + + Activity Level + + + + + + + Behavior & Emotion + + + {clientData.activity} + Current Activity + + + + + Last updated: {new Date().toLocaleTimeString()} + + + + + Location + + + {clientData.context} + + {clientData.latitude}, {clientData.longitude} + + + + + + +); diff --git a/pawsense/components/Health.jsx b/pawsense/components/Health.jsx new file mode 100644 index 0000000..a3a8fdb --- /dev/null +++ b/pawsense/components/Health.jsx @@ -0,0 +1,63 @@ +// @ts-check +import { ScrollView, Text, View } from "react-native"; +import { Card, ProgressBar } from "./components"; +import styles from "../AppStyles"; +import { Ionicons } from "@expo/vector-icons"; + +export default (clientData) => ( + + + Vital Signs + + + + + Heart Rate + + {clientData.heart_rate} + BPM (Normal) + + + + + Temperature + + {clientData.temperature}°F + Normal Range + + + + + + Daily Activity + + + Steps Today + {clientData.steps} + + + Goal: 15,000 steps + + + + Calories Burned + {clientData.calories} + + + Goal: 600 calories + + + + + Health Alerts + + + ⚠️ Health Alert! + + + Max has missed his medicine dose today. + + + + +); diff --git a/pawsense/components/Location.jsx b/pawsense/components/Location.jsx new file mode 100644 index 0000000..df55a88 --- /dev/null +++ b/pawsense/components/Location.jsx @@ -0,0 +1,55 @@ +import { ScrollView, Text, View } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import { Card, Badge } from "./components"; +import styles from "../AppStyles"; + +export default (clientData, locations) => ( + + + Live Location + + + {clientData.context} + + {clientData.latitude}, {clientData.longitude} + + + + + + Distance from Home + 0.3 miles + + + Last Movement + 2 minutes ago + + + + + + Recent Locations + {locations.map((location, index) => ( + + + + + {location.name} + + {Math.floor(Math.random() * 60)} min ago + + + + + + ))} + + +); diff --git a/pawsense/components/Translator.jsx b/pawsense/components/Translator.jsx new file mode 100644 index 0000000..a802121 --- /dev/null +++ b/pawsense/components/Translator.jsx @@ -0,0 +1,70 @@ +import { ScrollView, Text, View, TouchableOpacity, TextInput } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import styles from "../AppStyles"; +import { Card, Badge } from "./components"; + +export default (translatorInput, setTranslatorInput, handleTranslate, translatorOutput, recentBarks) => ( + + + Two-Way Translator + + Speak to Max: + + + + + + + + {translatorOutput && ( + + Max's Response: + {translatorOutput} + + )} + + + + Recent Bark Translations + {recentBarks.map((bark, index) => ( + + + {bark.translation} + + + {bark.time} + + ))} + + + + Quick Commands + + {['Sit', 'Stay', 'Come', 'Good Boy'].map((command) => ( + { + setTranslatorInput(command); + handleTranslate(); + }} + > + {command} + + ))} + + + +); diff --git a/pawsense/components/components.jsx b/pawsense/components/components.jsx new file mode 100644 index 0000000..1b82728 --- /dev/null +++ b/pawsense/components/components.jsx @@ -0,0 +1,40 @@ +import { View, Text, TouchableOpacity } from "react-native"; +import { Ionicons } from "@expo/vector-icons"; +import styles from "../AppStyles"; + +export const Card = ({ children, style }) => ( + + {children} + +); + +export const Badge = ({ text, style, textStyle }) => ( + + {text} + +); + +export const ProgressBar = ({ progress, color = '#3B82F6', height = 8 }) => ( + + + +); + +export const TabButton = ({ icon, isActive, onPress, label }) => ( + + + {label && {label}} + +);