Skip to content

Frontend: Phase 1 for Issue #151 – centralized API client & hooks#174

Open
dheeraj12347 wants to merge 5 commits intoAOSSIE-Org:mainfrom
dheeraj12347:refactor/api-layer-architecture
Open

Frontend: Phase 1 for Issue #151 – centralized API client & hooks#174
dheeraj12347 wants to merge 5 commits intoAOSSIE-Org:mainfrom
dheeraj12347:refactor/api-layer-architecture

Conversation

@dheeraj12347
Copy link

@dheeraj12347 dheeraj12347 commented Jan 24, 2026

This PR implements the first step of the 5-layer frontend architecture described in Issue #151:

Adds a centralized apiClient in src/services/apiClient.js.

Adds useWeight and useAppointments hooks in src/hooks/ for weight and appointment CRUD using the centralized client.

Scope:

No RAGService.js changes (Intelligence layer will be handled by @sharma-sugurthi).

No dateUtils changes (handled by @Vikas4245).

No major Screen refactors yet; those will come in follow-up PRs.

Related: #151

Summary by CodeRabbit

  • New Features

    • Enhanced appointment management system for tracking and organizing appointments.
    • Weight tracking system with record management capabilities.
    • Health metrics tracking with historical data retrieval.
    • Improved API infrastructure with centralized client and authentication support.
    • Date utility functions for formatting and time calculations.
  • Dependencies

    • Updated axios library to the latest compatible version.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 24, 2026

📝 Walkthrough

Walkthrough

A new API client infrastructure is introduced alongside modular endpoint wrappers for appointments, weight, and health data. Custom React hooks manage state and CRUD operations for appointments and weight records. Date utility functions provide formatting and time-calculation helpers. axios dependency is updated to ^1.13.2.

Changes

Cohort / File(s) Summary
Dependency Updates
Frontend/package.json
Updated axios from ^1.8.2 to ^1.13.2
API Client Infrastructure
Frontend/src/api/client.js, Frontend/src/services/apiClient.js
Introduced dual HTTP client layers: client.js uses fetch with base URL config; apiClient.js uses axios with token auth, interceptors, and error logging
API Endpoint Modules
Frontend/src/api/appointments.js, Frontend/src/api/health.js, Frontend/src/api/weight.js
Created modular CRUD wrappers for appointments (getAll, getById, create, update, delete), health (getMetrics, updateMetrics, getHistory), and weight (getRecords, addRecord, updateRecord, deleteRecord)
API Aggregator
Frontend/src/api/index.js
Centralized re-exports for all API modules and client for convenient imports
React Hooks
Frontend/src/hooks/useAppointments.js, Frontend/src/hooks/useWeight.js
Introduced state management hooks with CRUD operations, loading/error handling, and babyId validation for appointments and weight records
Hook Exports
Frontend/src/hooks/index.js
Added barrel exports for useAppointments and useWeight hooks
Date Utilities
Frontend/src/utils/dateUtils.js
New utility object with formatDate, formatDateTime, isToday, daysBetween, toISOString, and getRelativeTime methods

Sequence Diagram

sequenceDiagram
    participant Component
    participant Hook as useAppointments
    participant AppAPI as appointmentsAPI
    participant Client as apiClient
    participant Server as Backend API

    Component->>Hook: fetchAppointments(babyId)
    activate Hook
    Hook->>Hook: ensureBabyId validation
    Hook->>Hook: setLoading(true)
    Hook->>AppAPI: getAll()
    activate AppAPI
    AppAPI->>Client: get(/appointments)
    deactivate AppAPI
    activate Client
    Client->>Server: GET /appointments
    deactivate Client
    Server-->>Client: appointments data
    activate Client
    Client->>Client: parse response
    deactivate Client
    Client-->>AppAPI: parsed data
    AppAPI-->>Hook: appointments array
    Hook->>Hook: setAppointments(data)
    Hook->>Hook: setLoading(false)
    deactivate Hook
    Hook-->>Component: state updated
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A brand new API warren awaits,
With hooks that manage weight and date,
Appointments dance through fetch and grace,
While utilities format time and space—
All wired up in modular bliss! 🎉

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main objective: implementing Phase 1 of a centralized API client and hooks architecture for the frontend, directly addressing Issue #151.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@Frontend/src/api/client.js`:
- Around line 70-88: The delete method (async delete(endpoint)) currently calls
response.json() unconditionally which will throw on 204 No Content or empty
bodies; change the success-path to detect empty responses (e.g., response.status
=== 204 or zero-length body via response.headers or by calling response.text()
and checking for an empty string) and return a sensible value (null or {})
instead of parsing JSON when empty, otherwise parse and return response.json();
keep the existing error handling for non-ok responses.

In `@Frontend/src/api/health.js`:
- Around line 12-13: The getHistory function builds the request URL with raw
interpolation which can break for ISO strings or characters like + and spaces;
update getHistory (the exported method calling apiClient.get) to encode query
parameters before appending them—either use encodeURIComponent on startDate and
endDate or build the query via URLSearchParams and append the resulting
?start=...&end=... to `/health/${userId}/history`—so the apiClient.get call
always receives a properly encoded URL.

In `@Frontend/src/api/weight.js`:
- Around line 7-15: The weightAPI export in Frontend/src/api/weight.js is unused
and duplicates functionality in useWeight.js with mismatched endpoints and
parameter names; either delete weightAPI or consolidate by updating useWeight.js
to call weightAPI.getRecords/addRecord/updateRecord/deleteRecord (which use
apiClient) and align endpoints/params (change weightAPI endpoints from
`/weight/${userId}` etc. to `/babies/${babyId}/weights` and rename
userId→babyId) or vice‑versa—remove the dead module if you prefer deletion.
Ensure the chosen functions (weightAPI.getRecords, addRecord, updateRecord,
deleteRecord) are imported where useWeight.js currently performs axios calls and
that apiClient is the single axios instance used.

In `@Frontend/src/services/apiClient.js`:
- Around line 28-33: The response interceptor in apiClient
(apiClient.interceptors.response.use) currently logs the full error object
(fallback || error), which can leak Authorization headers; change the log to
only safe fields: error.message, error.response?.status, error.response?.data
(or a trimmed summary), and request identifiers like error.config?.url/method
but do NOT log error.config.headers or the entire error object; if you need
headers, explicitly copy and redact Authorization (e.g., replace with
"[REDACTED]") before logging; keep returning Promise.reject(error) unchanged so
error propagation isn't affected.

In `@Frontend/src/utils/dateUtils.js`:
- Around line 50-53: The time-ago branch returns plural units regardless of
count, causing "1 minutes ago" etc.; update the returns that use diffInSeconds
(the minute/hour/day branches) to choose singular when the computed value equals
1 (e.g., const mins = Math.floor(diffInSeconds/60); return `${mins} ${mins === 1
? 'minute' : 'minutes'} ago`; do similarly for hours and days) so the strings
correctly use "minute"/"hour"/"day" for 1 and plural otherwise.
🧹 Nitpick comments (8)
Frontend/src/utils/dateUtils.js (3)

45-55: Future dates fall through to formatDate silently.

When date is in the future, diffInSeconds will be negative, causing all conditions to fail and falling through to formatDate. This may not be the intended behavior—consider either handling future dates explicitly or documenting this behavior.

Option: Add explicit handling for future dates
  getRelativeTime: (date) => {
    const now = new Date();
    const past = new Date(date);
    const diffInSeconds = Math.floor((now - past) / 1000);

+   if (diffInSeconds < 0) {
+     return dateUtils.formatDate(date); // or return 'in the future'
+   }
+
    if (diffInSeconds < 60) return 'just now';

34-39: Math.ceil may produce surprising results for same-day dates.

Using Math.ceil means that any non-zero time difference (even 1 millisecond) returns at least 1 day. For example, two timestamps on the same calendar day but a few hours apart could return 1 instead of 0. Consider whether Math.round or Math.floor better fits the expected semantics.


5-56: Consider adding input validation for robustness.

All methods pass input directly to new Date() without validation. Invalid inputs (e.g., null, undefined, malformed strings) will produce "Invalid Date" strings or NaN results. If these utilities will be consumed broadly, defensive checks could prevent subtle bugs.

Example: Add a validation helper
const parseDate = (date) => {
  const parsed = new Date(date);
  if (isNaN(parsed.getTime())) {
    throw new Error(`Invalid date: ${date}`);
  }
  return parsed;
};

// Then use parseDate(date) instead of new Date(date) in each method
Frontend/src/services/apiClient.js (1)

5-6: Use platform-aware fallback instead of hardcoded localhost.
On Android Emulator and physical Android devices, localhost points to the device itself, not your host machine—this silently breaks API calls if API_BASE_URL is unset. The Android Emulator requires 10.0.2.2 to reach the host.

♻️ Recommended adjustment
import axios from "axios";
+import { Platform } from "react-native";
 import Config from "react-native-config";

-const API_BASE_URL = Config.API_URL || "http://localhost:5000/api";
+const FALLBACK_BASE_URL = Platform.select({
+  android: "http://10.0.2.2:5000/api",
+  ios: "http://localhost:5000/api",
+  default: "http://localhost:5000/api",
+});
+const API_BASE_URL = Config.API_URL || FALLBACK_BASE_URL;

For physical devices, rely on Config.API_URL (e.g., via .env file or build-time configuration) to specify the correct LAN IP or use adb reverse for USB-connected devices.

Frontend/src/api/client.js (1)

7-26: Missing authentication header support.

The API client doesn't include an Authorization header for authenticated endpoints. Consider adding a mechanism to attach auth tokens (e.g., from localStorage or a context) to requests.

♻️ Suggested approach
 export const apiClient = {
   async get(endpoint) {
     try {
+      const token = localStorage.getItem('authToken');
       const response = await fetch(`${API_BASE_URL}${endpoint}`, {
         method: 'GET',
         headers: {
           'Content-Type': 'application/json',
+          ...(token && { 'Authorization': `Bearer ${token}` }),
         },
       });

Or better, extract a shared getHeaders() helper used by all methods.

Frontend/src/hooks/useWeight.js (2)

1-5: Architectural inconsistency: not using the new weightAPI module.

This hook imports from ../services/apiClient (axios-based), but the PR also introduces src/api/weight.js with weightAPI that provides the same CRUD operations. Consider using the dedicated API module to maintain a clear layered architecture:

Hooks (useWeight) → API modules (weightAPI) → Client (apiClient)
♻️ Suggested refactor using weightAPI
 // src/hooks/useWeight.js
 import { useState, useCallback } from "react";
-import apiClient from "../services/apiClient";
+import { weightAPI } from "../api/weight";

Then update the methods to use weightAPI.getRecords(babyId), etc. (after aligning endpoint paths).


6-8: Consider separate loading states for concurrent operations.

Using a single loading flag for all CRUD operations means that if a user triggers addWeight while fetchWeights is still in progress, the loading state becomes interleaved and could lead to unexpected UI behavior.

♻️ Possible improvement
 const [weights, setWeights] = useState([]);
-const [loading, setLoading] = useState(false);
+const [isLoading, setIsLoading] = useState({
+  fetch: false,
+  add: false,
+  update: false,
+  delete: false,
+});
 const [error, setError] = useState(null);

Or use a counter-based approach: loadingCount incremented/decremented per operation.

Frontend/src/hooks/useAppointments.js (1)

1-4: Same architectural note as useWeight: consider using dedicated API modules.

This hook directly uses apiClient instead of going through an appointmentsAPI module (if one exists in src/api/appointments.js). For consistency with the layered architecture this PR introduces, consider routing through the API module layer.

Comment on lines +70 to +88
async delete(endpoint) {
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return await response.json();
} catch (error) {
console.error(`API DELETE ${endpoint} failed:`, error);
throw error;
}
},
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

DELETE may fail on 204 No Content responses.

Many REST APIs return 204 No Content with an empty body for successful DELETE operations. Calling response.json() on an empty body will throw a parsing error.

🐛 Proposed fix
   async delete(endpoint) {
     try {
       const response = await fetch(`${API_BASE_URL}${endpoint}`, {
         method: 'DELETE',
         headers: {
           'Content-Type': 'application/json',
         },
       });
       
       if (!response.ok) {
         throw new Error(`HTTP ${response.status}: ${response.statusText}`);
       }
       
-      return await response.json();
+      // Handle 204 No Content responses
+      if (response.status === 204) {
+        return null;
+      }
+      const text = await response.text();
+      return text ? JSON.parse(text) : null;
     } catch (error) {
       console.error(`API DELETE ${endpoint} failed:`, error);
       throw error;
     }
   },
📝 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
async delete(endpoint) {
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error(`API DELETE ${endpoint} failed:`, error);
throw error;
}
},
async delete(endpoint) {
try {
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Handle 204 No Content responses
if (response.status === 204) {
return null;
}
const text = await response.text();
return text ? JSON.parse(text) : null;
} catch (error) {
console.error(`API DELETE ${endpoint} failed:`, error);
throw error;
}
},
🤖 Prompt for AI Agents
In `@Frontend/src/api/client.js` around lines 70 - 88, The delete method (async
delete(endpoint)) currently calls response.json() unconditionally which will
throw on 204 No Content or empty bodies; change the success-path to detect empty
responses (e.g., response.status === 204 or zero-length body via
response.headers or by calling response.text() and checking for an empty string)
and return a sensible value (null or {}) instead of parsing JSON when empty,
otherwise parse and return response.json(); keep the existing error handling for
non-ok responses.

Comment on lines +12 to +13
getHistory: (userId, startDate, endDate) =>
apiClient.get(`/health/${userId}/history?start=${startDate}&end=${endDate}`),
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

Encode query params in getHistory (Lines 12-13).
Raw string interpolation can break for ISO strings or values containing +, spaces, etc.

🔧 Safer query building
-  getHistory: (userId, startDate, endDate) => 
-    apiClient.get(`/health/${userId}/history?start=${startDate}&end=${endDate}`),
+  getHistory: (userId, startDate, endDate) => 
+    apiClient.get(
+      `/health/${userId}/history?start=${encodeURIComponent(startDate)}&end=${encodeURIComponent(endDate)}`
+    ),
🤖 Prompt for AI Agents
In `@Frontend/src/api/health.js` around lines 12 - 13, The getHistory function
builds the request URL with raw interpolation which can break for ISO strings or
characters like + and spaces; update getHistory (the exported method calling
apiClient.get) to encode query parameters before appending them—either use
encodeURIComponent on startDate and endDate or build the query via
URLSearchParams and append the resulting ?start=...&end=... to
`/health/${userId}/history`—so the apiClient.get call always receives a properly
encoded URL.

Comment on lines +7 to +15
export const weightAPI = {
getRecords: (userId) => apiClient.get(`/weight/${userId}`),

addRecord: (weightData) => apiClient.post('/weight', weightData),

updateRecord: (id, weightData) => apiClient.put(`/weight/${id}`, weightData),

deleteRecord: (id) => apiClient.delete(`/weight/${id}`),
};
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:

# Search for weightAPI imports and usage
rg "weightAPI" --type=js --type=jsx

# Search for imports from api/weight module
rg "from.*['\"].*api/weight['\"]" --type=js --type=jsx

# Search for weight api imports
rg "from.*['\"].*api/weight['\"]" --type=js

Repository: AOSSIE-Org/BabyNest

Length of output: 118


🏁 Script executed:

# Check if useWeight.js exists and examine its endpoints
find . -name "useWeight.js" -type f

# Also search for any weight-related API imports
rg "import.*weight" --type=js --type=jsx -A 2

Repository: AOSSIE-Org/BabyNest

Length of output: 141


🏁 Script executed:

# Search for weightAPI without file type restrictions
rg "weightAPI"

# Search for imports from api/weight
rg "from.*api/weight"

# Search for weight-related imports
rg "import.*weight"

Repository: AOSSIE-Org/BabyNest

Length of output: 248


🏁 Script executed:

# Examine useWeight.js
cat -n ./Frontend/src/hooks/useWeight.js

# Examine weight.js
cat -n ./Frontend/src/api/weight.js

# Check what's in the api directory
ls -la ./Frontend/src/api/

Repository: AOSSIE-Org/BabyNest

Length of output: 4483


Remove or consolidate the unused weightAPI module—it duplicates functionality already implemented in useWeight hook with mismatched endpoints.

The weightAPI exported from this file is never imported anywhere in the codebase. Meanwhile, useWeight.js implements the same functionality using:

  • Different endpoints: /babies/${babyId}/weights vs /weight/${userId}
  • Different parameter patterns: babyId vs userId
  • The axios-based apiClient from ../services/apiClient

This creates orphaned dead code. Either delete this module or refactor useWeight.js to use it instead.

🤖 Prompt for AI Agents
In `@Frontend/src/api/weight.js` around lines 7 - 15, The weightAPI export in
Frontend/src/api/weight.js is unused and duplicates functionality in
useWeight.js with mismatched endpoints and parameter names; either delete
weightAPI or consolidate by updating useWeight.js to call
weightAPI.getRecords/addRecord/updateRecord/deleteRecord (which use apiClient)
and align endpoints/params (change weightAPI endpoints from `/weight/${userId}`
etc. to `/babies/${babyId}/weights` and rename userId→babyId) or
vice‑versa—remove the dead module if you prefer deletion. Ensure the chosen
functions (weightAPI.getRecords, addRecord, updateRecord, deleteRecord) are
imported where useWeight.js currently performs axios calls and that apiClient is
the single axios instance used.

Comment on lines +28 to +33
// Log and forward errors
apiClient.interceptors.response.use(
(response) => response,
(error) => {
console.error("API Error:", error?.response || error?.message || error);
return Promise.reject(error);
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:

Axios error object structure response config Authorization header logging

💡 Result:

In Axios, a thrown error is an AxiosError whose key fields are typically: message, name, stack, config, code, status, plus (depending on what happened) response and/or request. If the server responded with a non-2xx status, you’ll usually get error.response (with data, status, headers), and Axios also exposes the original request config as error.config. (axios-http.com)

Where the Authorization header shows up

If you set auth on the request (defaults, instance defaults, or an interceptor), it can end up in multiple places you might accidentally log:

  • error.config.headers (most common / easiest to access) (axios-http.com)
  • In Node.js, also potentially inside the underlying request’s raw header string (e.g. error.request._header and related internals), which people often discover when trying to log the whole error object (github.com)

Axios maintainers/users have explicitly raised that failed-request errors can leak API keys/tokens because headers appear in config and in nested request objects, making the error unsafe to log “as-is”. (github.com)

Safe pattern: redact before logging

Redact at minimum config.headers.Authorization (and similar) before you serialize/log:

function redactAxiosError(err) {
  const cfg = err?.config || {};
  const headers = { ...(cfg.headers || {}) };

  // Common variants
  if (headers.Authorization) headers.Authorization = "[REDACTED]";
  if (headers.authorization) headers.authorization = "[REDACTED]";

  return {
    name: err.name,
    message: err.message,
    code: err.code,
    status: err.response?.status ?? err.status,
    config: { ...cfg, headers },
    // include response details if you want, but avoid echoing config/headers again
    response: err.response
      ? { status: err.response.status, data: err.response.data, headers: err.response.headers }
      : undefined,
  };
}

// usage
try {
  await axios.get("/...");
} catch (err) {
  console.log(redactAxiosError(err));
}

If you currently do console.log(error) or JSON.stringify(error) directly, switch to logging a whitelisted/sanitized shape like above, because error.config (and sometimes nested request internals) can contain Authorization. (axios-http.com)

Citations:


🏁 Script executed:

# Check if the file exists and review the context
head -50 Frontend/src/services/apiClient.js

Repository: AOSSIE-Org/BabyNest

Length of output: 1000


Avoid logging full error object; may leak Authorization headers (Lines 28-33).

The fallback || error logs the entire error object, which includes error.config.headers.Authorization in Axios errors. Logging this exposes API tokens/keys in production logs. While error.response and error.message are safe, the full error object must not be logged verbatim.

🔒 Suggested safe logging
 apiClient.interceptors.response.use(
   (response) => response,
   (error) => {
-    console.error("API Error:", error?.response || error?.message || error);
+    const status = error?.response?.status;
+    const url = error?.config?.url;
+    console.error("API Error:", { status, url, message: error?.message });
     return Promise.reject(error);
   }
 );
📝 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
// Log and forward errors
apiClient.interceptors.response.use(
(response) => response,
(error) => {
console.error("API Error:", error?.response || error?.message || error);
return Promise.reject(error);
// Log and forward errors
apiClient.interceptors.response.use(
(response) => response,
(error) => {
const status = error?.response?.status;
const url = error?.config?.url;
console.error("API Error:", { status, url, message: error?.message });
return Promise.reject(error);
🤖 Prompt for AI Agents
In `@Frontend/src/services/apiClient.js` around lines 28 - 33, The response
interceptor in apiClient (apiClient.interceptors.response.use) currently logs
the full error object (fallback || error), which can leak Authorization headers;
change the log to only safe fields: error.message, error.response?.status,
error.response?.data (or a trimmed summary), and request identifiers like
error.config?.url/method but do NOT log error.config.headers or the entire error
object; if you need headers, explicitly copy and redact Authorization (e.g.,
replace with "[REDACTED]") before logging; keep returning Promise.reject(error)
unchanged so error propagation isn't affected.

Comment on lines 50 to 53
if (diffInSeconds < 60) return 'just now';
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`;
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

Grammar issues with singular time units.

When the count is 1, the output will be "1 minutes ago", "1 hours ago", or "1 days ago" instead of the grammatically correct singular forms.

Proposed fix for singular/plural handling
-    if (diffInSeconds < 60) return 'just now';
-    if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
-    if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
-    if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`;
+    if (diffInSeconds < 60) return 'just now';
+    const minutes = Math.floor(diffInSeconds / 60);
+    if (diffInSeconds < 3600) return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`;
+    const hours = Math.floor(diffInSeconds / 3600);
+    if (diffInSeconds < 86400) return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`;
+    const days = Math.floor(diffInSeconds / 86400);
+    if (diffInSeconds < 604800) return `${days} ${days === 1 ? 'day' : 'days'} ago`;
📝 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
if (diffInSeconds < 60) return 'just now';
if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
if (diffInSeconds < 604800) return `${Math.floor(diffInSeconds / 86400)} days ago`;
if (diffInSeconds < 60) return 'just now';
const minutes = Math.floor(diffInSeconds / 60);
if (diffInSeconds < 3600) return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'} ago`;
const hours = Math.floor(diffInSeconds / 3600);
if (diffInSeconds < 86400) return `${hours} ${hours === 1 ? 'hour' : 'hours'} ago`;
const days = Math.floor(diffInSeconds / 86400);
if (diffInSeconds < 604800) return `${days} ${days === 1 ? 'day' : 'days'} ago`;
🤖 Prompt for AI Agents
In `@Frontend/src/utils/dateUtils.js` around lines 50 - 53, The time-ago branch
returns plural units regardless of count, causing "1 minutes ago" etc.; update
the returns that use diffInSeconds (the minute/hour/day branches) to choose
singular when the computed value equals 1 (e.g., const mins =
Math.floor(diffInSeconds/60); return `${mins} ${mins === 1 ? 'minute' :
'minutes'} ago`; do similarly for hours and days) so the strings correctly use
"minute"/"hour"/"day" for 1 and plural otherwise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant