Frontend: Phase 1 for Issue #151 – centralized API client & hooks#174
Frontend: Phase 1 for Issue #151 – centralized API client & hooks#174dheeraj12347 wants to merge 5 commits intoAOSSIE-Org:mainfrom
Conversation
📝 WalkthroughWalkthroughA 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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 toformatDatesilently.When
dateis in the future,diffInSecondswill be negative, causing all conditions to fail and falling through toformatDate. 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.ceilmay produce surprising results for same-day dates.Using
Math.ceilmeans 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 whetherMath.roundorMath.floorbetter 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 orNaNresults. 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 methodFrontend/src/services/apiClient.js (1)
5-6: Use platform-aware fallback instead of hardcoded localhost.
On Android Emulator and physical Android devices,localhostpoints to the device itself, not your host machine—this silently breaks API calls ifAPI_BASE_URLis unset. The Android Emulator requires10.0.2.2to 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.envfile or build-time configuration) to specify the correct LAN IP or useadb reversefor USB-connected devices.Frontend/src/api/client.js (1)
7-26: Missing authentication header support.The API client doesn't include an
Authorizationheader 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 newweightAPImodule.This hook imports from
../services/apiClient(axios-based), but the PR also introducessrc/api/weight.jswithweightAPIthat 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
loadingflag for all CRUD operations means that if a user triggersaddWeightwhilefetchWeightsis 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:
loadingCountincremented/decremented per operation.Frontend/src/hooks/useAppointments.js (1)
1-4: Same architectural note asuseWeight: consider using dedicated API modules.This hook directly uses
apiClientinstead of going through anappointmentsAPImodule (if one exists insrc/api/appointments.js). For consistency with the layered architecture this PR introduces, consider routing through the API module layer.
| 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; | ||
| } | ||
| }, |
There was a problem hiding this comment.
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.
| 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.
| getHistory: (userId, startDate, endDate) => | ||
| apiClient.get(`/health/${userId}/history?start=${startDate}&end=${endDate}`), |
There was a problem hiding this comment.
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.
| 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}`), | ||
| }; |
There was a problem hiding this comment.
🧩 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=jsRepository: 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 2Repository: 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}/weightsvs/weight/${userId} - Different parameter patterns:
babyIdvsuserId - The axios-based
apiClientfrom../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.
| // Log and forward errors | ||
| apiClient.interceptors.response.use( | ||
| (response) => response, | ||
| (error) => { | ||
| console.error("API Error:", error?.response || error?.message || error); | ||
| return Promise.reject(error); |
There was a problem hiding this comment.
🧩 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._headerand 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:
- 1: https://axios-http.com/docs/handling_errors?utm_source=openai
- 2: https://axios-http.com/docs/handling_errors?utm_source=openai
- 3: Remove API key from headers in error message axios/axios#4249
- 4: Remove API key from headers in error message axios/axios#4249
- 5: https://axios-http.com/docs/handling_errors?utm_source=openai
🏁 Script executed:
# Check if the file exists and review the context
head -50 Frontend/src/services/apiClient.jsRepository: 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.
| // 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.
Frontend/src/utils/dateUtils.js
Outdated
| 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`; |
There was a problem hiding this comment.
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.
| 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.
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
Dependencies
✏️ Tip: You can customize this high-level summary in your review settings.