Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions Frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@react-navigation/stack": "^7.1.1",
"@xenova/transformers": "^2.17.2",
"axios": "^1.8.2",
"dayjs": "^1.11.19",
"expo-font": "^13.0.3",
"llama.rn": "^0.5.4",
"lottie-react-native": "^7.2.2",
Expand Down
161 changes: 107 additions & 54 deletions Frontend/src/Screens/WeightScreen.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ import {
Alert,
} from 'react-native';
import {TextInput, Button, Card, Dialog, Portal} from 'react-native-paper';
import {BASE_URL} from '@env';
import HeaderWithBack from '../Components/HeaderWithBack';
import Icon from 'react-native-vector-icons/Ionicons';
import {weightApi} from '../services/weightApi';
import dayjs from 'dayjs';

export default function WeightScreen() {
const [week, setWeek] = useState('');
Expand All @@ -20,39 +21,35 @@ export default function WeightScreen() {
const [refreshing, setRefreshing] = useState(false);

const [editVisible, setEditVisible] = useState(false);
const [editData, setEditData] = useState(null);
const [editData, setEditData] = useState({
id: null,
week_number: '',
weight: '',
note: '',
});

const [loading, setLoading] = useState(false);

const fetchWeightHistory = async () => {
try {
const res = await fetch(`${BASE_URL}/weight`);
if (!res.ok) {
throw new Error('Network response was not ok');
}
const data = await res.json();
setHistory(data.reverse());
setLoading(true);
const data = await weightApi.getAll();
setHistory([...data].reverse());
} catch (err) {
console.error('Failed to fetch weights:', err);
Alert.alert('Error', `unable to laod weight history`);
} finally {
Comment on lines 49 to +51
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix typo in error alert copy

Line 40: “laod” → “load”.

✏️ Suggested fix
-      Alert.alert('Error', `unable to laod weight history`);
+      Alert.alert('Error', 'Unable to load weight history');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error('Failed to fetch weights:', err);
Alert.alert('Error', `unable to laod weight history`);
} finally {
console.error('Failed to fetch weights:', err);
Alert.alert('Error', 'Unable to load weight history');
} finally {
🤖 Prompt for AI Agents
In `@Frontend/src/Screens/WeightScreen.jsx` around lines 39 - 41, There is a typo
in the error Alert message inside the WeightScreen component's fetch error
handling: change the Alert.alert call that currently reads "unable to laod
weight history" to "Unable to load weight history" (or at minimum fix "laod" →
"load") in the catch block where console.error('Failed to fetch weights:', err)
and Alert.alert are used.

setLoading(false);
}
};


const formatLocalDate = (utcDateString) => {
const formatLocalDate = utcDateString => {
if (!utcDateString) return '';
const dateStringWithZ = utcDateString.endsWith('Z') ? utcDateString : `${utcDateString}Z`;
const date = new Date(dateStringWithZ);
if (isNaN(date.getTime())) return 'Invalid date';
return date.toLocaleString(undefined, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true,
});
const date = dayjs(utcDateString); // automatically parses ISO strings
if (!date.isValid()) return 'Invalid date';
return date.format('YYYY-MM-DD HH:mm:ss');
};


useEffect(() => {
fetchWeightHistory();
}, []);
Expand All @@ -64,24 +61,47 @@ export default function WeightScreen() {
};

const handleSubmit = async () => {
if (!week || !weight) {
Alert.alert('Error', 'Please fill in all required fields');
const weekNum = Number(week);
const weightNum = Number(weight);

if (!Number.isInteger(weekNum) || weekNum < 1 || weekNum > 40) {
Alert.alert('Invalid week', 'Week must be between 1 and 40');
return;
}

if (weightNum <= 0) {
Alert.alert('Invalid weight', 'Weight must be greater than 0');
return;
}
Comment on lines 84 to +96
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -t f WeightScreen.jsx

Repository: AOSSIE-Org/BabyNest

Length of output: 100


🏁 Script executed:

wc -l Frontend/src/Screens/WeightScreen.jsx 2>/dev/null || echo "File not found or path needs correction"

Repository: AOSSIE-Org/BabyNest

Length of output: 104


🌐 Web query:

JavaScript Number.isFinite NaN behavior compared to isNaN and less-than comparison with NaN

💡 Result:

  • Number.isFinite(value)

    • Returns true only for values whose type is number and that are neither NaN nor ±Infinity; it does not coerce non-numbers.
    • So: Number.isFinite(NaN) === false, Number.isFinite("0") === false. [1]
  • Global isNaN(value) vs Number.isNaN(value)

    • Global isNaN coerces its argument to a number first, so many non-numbers become NaN and produce true (e.g. isNaN("blabla") === true). [2]
    • Number.isNaN does not coerce and is only true when the value is actually the numeric NaN (e.g. Number.isNaN("NaN") === false, Number.isNaN(NaN) === true). [3]
  • Relational comparisons with NaN (<, >, <=, >=)

    • If either operand becomes NaN, the comparison returns false.
    • So: NaN < 3 is false, 3 < NaN is false (and similarly for the other relational operators). [4]

Sources: [1] [2] [3] [4]


🏁 Script executed:

sed -n '60,80p' Frontend/src/Screens/WeightScreen.jsx

Repository: AOSSIE-Org/BabyNest

Length of output: 613


🏁 Script executed:

sed -n '135,155p' Frontend/src/Screens/WeightScreen.jsx

Repository: AOSSIE-Org/BabyNest

Length of output: 614


Add NaN validation to reject non-numeric weight inputs in create/update operations

When users enter non-numeric input, Number('abc') returns NaN. Since NaN <= 0 evaluates to false, the validation passes and invalid weight values are sent to the API. Both the handleSubmit (create) and handleUpdate methods have this vulnerability.

🔧 Suggested fix
-    if (weightNum <= 0) {
+    if (!Number.isFinite(weightNum) || weightNum <= 0) {
       Alert.alert('Invalid weight', 'Weight must be greater than 0');
       return;
     }

Apply this fix at both locations: lines 63-75 (handleSubmit) and lines 135-150 (handleUpdate).

🤖 Prompt for AI Agents
In `@Frontend/src/Screens/WeightScreen.jsx` around lines 63 - 75, The weight
validation currently treats Number('abc') as NaN which bypasses the `weightNum
<= 0` check; update both handleSubmit and handleUpdate to explicitly reject
non-numeric weights by checking Number.isFinite(weightNum) (or
!Number.isNaN(weightNum)) before the >0 check, and show the same Alert when the
value is not a finite positive number so non-numeric inputs are not sent to the
API.


const tempId = Date.now(); // temporary id for optimistic UI
const newEntry = {
id: tempId,
week_number: weekNum,
weight: weightNum,
note,
created_at: new Date().toISOString(),
};

// Optimistic update
setHistory(prev => [newEntry, ...prev]);

try {
await fetch(`${BASE_URL}/weight`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({week_number: week, weight, note}),
const savedEntry = await weightApi.create({
week_number: weekNum,
weight: weightNum,
note,
});
setWeek('');
setWeight('');
setNote('');
fetchWeightHistory();
// Replace temp entry with real id from server
setHistory(prev => prev.map(e => (e.id === tempId ? savedEntry : e)));
} catch (err) {
setHistory(prev => prev.filter(e => e.id !== tempId));
console.error('Failed to save weight:', err);
Alert.alert('Error', 'Failed to save weight entry. Please try again.');
} finally {
setWeek('');
setWeight('');
setNote('');
}
};

Expand All @@ -95,12 +115,12 @@ export default function WeightScreen() {
text: 'Delete',
style: 'destructive',
onPress: async () => {
const prevHistory = [...history];
setHistory(prev => prev.filter(e => e.id !== id));
try {
await fetch(`${BASE_URL}/weight/${id}`, {
method: 'DELETE',
});
fetchWeightHistory();
await weightApi.remove(id);
} catch (err) {
setHistory(prevHistory);
console.error('Failed to delete weight:', err);
Alert.alert('Error', 'Failed to delete entry. Please try again.');
}
Expand All @@ -116,28 +136,56 @@ export default function WeightScreen() {
};

const handleUpdate = async () => {
const weekNum = Number(editData.week_number);
const weightNum = Number(editData.weight);

if (!Number.isInteger(weekNum) || weekNum < 1 || weekNum > 40) {
Alert.alert('Invalid week', 'Week must be between 1 and 40');
return;
}

if (weightNum <= 0) {
Alert.alert('Invalid weight', 'Weight must be greater than 0');
return;
}

const prevHistory = [...history];
setHistory(prev =>
prev.map(e =>
e.id === editData.id
? {...e, week_number: weekNum, weight: weightNum, note: editData.note}
: e,
),
);
try {
await fetch(`${BASE_URL}/weight/${editData.id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
week_number: editData.week_number,
weight: editData.weight,
note: editData.note,
}),
await weightApi.update(editData.id, {
week_number: weekNum,
weight: weightNum,
note: editData.note,
});
setEditVisible(false);
setEditData(null);
fetchWeightHistory();
} catch (err) {
setHistory(prevHistory);
console.error('Failed to update weight:', err);
Alert.alert('Error', 'Failed to update entry. Please try again.');
} finally {
setEditVisible(false);
setEditData({
id: null,
week_number: '',
weight: '',
note: '',
});
}
};

return (
<View style={styles.container}>
<HeaderWithBack title="Weight Tracker" />
{loading && (
<View style={{paddingVertical: 30}}>
<Text style={{textAlign: 'center'}}>Loading weight history</Text>
</View>
)}
<ScrollView
contentContainerStyle={styles.content}
refreshControl={
Expand All @@ -156,7 +204,7 @@ export default function WeightScreen() {
mode="outlined"
left={<TextInput.Icon icon="calendar" />}
style={styles.input}
/>
/>
<TextInput
label="Weight (kg)"
value={weight}
Expand All @@ -179,17 +227,23 @@ export default function WeightScreen() {
<Button
mode="contained"
onPress={handleSubmit}
disabled={loading}
style={styles.button}
labelStyle={{fontWeight: 'bold', color: '#fff'}}>
Save Entry
</Button>
</Card.Content>
</Card>

{!loading && history.length === 0 && (
<Text style={{textAlign: 'center', marginTop: 20, color: '#777'}}>
No weight entries yet. Add your first entry above.
</Text>
)}
{/* History */}
<Text style={styles.historyTitle}>Your Weight History</Text>
{history.map((entry, index) => (
<Card key={index} style={styles.entryCard}>
{history.map(entry => (
<Card key={entry.id} style={styles.entryCard}>
<Card.Content>
<View style={styles.entryRowBetween}>
<View style={styles.entryRow}>
Expand Down Expand Up @@ -222,7 +276,7 @@ export default function WeightScreen() {
<Text style={styles.entryNote}>Note: {entry.note}</Text>
) : null}
<Text style={styles.entryDate}>
{formatLocalDate(entry.created_at)}
{formatLocalDate(entry.created_at)}
</Text>
</Card.Content>
</Card>
Expand All @@ -233,7 +287,7 @@ export default function WeightScreen() {
<Portal>
<Dialog visible={editVisible} onDismiss={() => setEditVisible(false)}>
<Dialog.Title>Edit Entry</Dialog.Title>
<Dialog.Content >
<Dialog.Content>
<TextInput
label="Week Number"
value={editData?.week_number?.toString() || ''}
Expand Down Expand Up @@ -292,7 +346,6 @@ const styles = StyleSheet.create({
backgroundColor: 'white',
marginBottom: 15,
borderRadius: 10,

},
noteInput: {
minHeight: 100,
Expand Down
50 changes: 50 additions & 0 deletions Frontend/src/services/weightApi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {BASE_URL} from '@env';

const request = async (endpoint, options = {}, retries = 1) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);

try {
const res = await fetch(`${BASE_URL}${endpoint}`, {
headers: {'Content-Type': 'application/json'},
signal: controller.signal,
...options,
});

if (!res.ok) {
const msg = await res.text();
throw new Error(msg || 'Request failed');
}

return res.json();
Comment on lines +14 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Fetch API Response.json() empty body 204 No Content behavior throws error

💡 Result:

response.json() rejects on a 204 No Content (or any empty body) because it reads the body to completion and parses it as JSON—and an empty string is not valid JSON, so you get a SyntaxError. MDN explicitly notes json() throws SyntaxError when the body can’t be parsed as JSON. [1] Also, by definition 204 responses do not include content. [2] The HTTP semantics spec (RFC 9110) likewise states that for status 204 “there is no content in the response.” [3]

Fix (client side): don’t call .json() when there’s no body

const res = await fetch(url);

if (res.status === 204) return null; // or undefined

// optionally also guard by content-type
const ct = res.headers.get("content-type") ?? "";
if (ct.includes("application/json")) return await res.json();

return await res.text();

Alternative: parse conditionally from text

const res = await fetch(url);
const text = await res.text();
const data = text ? JSON.parse(text) : null;

Fix (server side): if you want JSON, don’t return 204

If the client should always receive JSON (even “empty”), return 200 with {} / [] instead of 204, because 204 means no response content at all. [2][3]

Sources: [1] [2] [3]


🏁 Script executed:

# Find and examine the weightApi.js file
find . -name "weightApi.js" -type f

Repository: AOSSIE-Org/BabyNest

Length of output: 99


🏁 Script executed:

cat -n ./Frontend/src/services/weightApi.js

Repository: AOSSIE-Org/BabyNest

Length of output: 1569


Handle empty/204 responses before calling res.json()

Line 19 will throw SyntaxError when res.json() is called on a 204 No Content response (or any empty body). DELETE requests (line 46–49) typically receive 204 responses, making delete operations fail unexpectedly.

🔧 Suggested fix (safe parsing)
-    return res.json();
+    const text = await res.text();
+    if (!text) return null;
+    try {
+      return JSON.parse(text);
+    } catch {
+      return text;
+    }
🤖 Prompt for AI Agents
In `@Frontend/src/services/weightApi.js` around lines 14 - 19, The current
response handling always calls res.json(), which throws on 204/empty responses
(affecting DELETE in weightApi.js around the delete call at lines 46–49); update
the response handling block that currently does "if (!res.ok) { ... } return
res.json();" to first check for empty bodies (e.g., if res.status === 204 ||
res.headers.get('content-length') === '0' || await res.clone().text() === '')
and return an appropriate empty value (null or {}) instead of calling
res.json(), otherwise call res.json() for non-empty successful responses so
DELETE and other 204 responses don't raise a SyntaxError.

} catch (err) {
if (retries > 0) {
console.warn(
`Retrying request to ${endpoint}, attempts left: ${retries}`,
);
return request(endpoint, options, retries - 1);
}
Comment on lines +20 to +26
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard retries for non‑idempotent requests

Line 20: retrying POST can create duplicate entries if the server processed the first request but the client timed out. Consider limiting retries to idempotent methods (or set retries=0 for create).

🔧 Suggested fix (limit retries to idempotent methods)
-const request = async (endpoint, options = {}, retries = 1) => {
+const request = async (endpoint, options = {}, retries = 1) => {
+  const method = (options.method || 'GET').toUpperCase();
+  const canRetry = ['GET', 'PUT', 'DELETE', 'HEAD', 'OPTIONS'].includes(method);
   const controller = new AbortController();
   const timeout = setTimeout(() => controller.abort(), 10000);
@@
-  } catch (err) {
-    if (retries > 0) {
+  } catch (err) {
+    if (canRetry && retries > 0) {
       console.warn(
         `Retrying request to ${endpoint}, attempts left: ${retries}`,
       );
       return request(endpoint, options, retries - 1);
     }
     throw err;
   } finally {
     clearTimeout(timeout);
   }
 };

Also applies to: 35-40

🤖 Prompt for AI Agents
In `@Frontend/src/services/weightApi.js` around lines 20 - 26, The retry logic in
the request function currently retries all methods, which can re-submit
non‑idempotent requests (e.g., POST) and create duplicates; modify the catch
blocks inside request to check options.method (or default to 'GET') and only
decrement retries/recursively retry for idempotent methods (GET, HEAD, PUT,
DELETE, OPTIONS) while avoiding retries for POST/PATCH or when
options.safeToRetry is false; apply the same guard to the second catch block
referenced around the other retry call so both retry paths use the method check
before calling request(endpoint, options, retries - 1).

throw err;
} finally {

clearTimeout(timeout);
}
};

export const weightApi = {
getAll: () => request('/weight', {}, 2),
create: data =>
request('/weight', {
method: 'POST',
body: JSON.stringify(data),
}),
update: (id, data) =>
request(`/weight/${id}`, {
method: 'PUT',
body: JSON.stringify(data),
}),
remove: id =>
request(`/weight/${id}`, {
method: 'DELETE',
}),
};