chore: fix all Biome lint warnings and MedDetail intake bell icons (#265)
- Backend: refactor nested ternaries, remove unused imports/any types - Frontend: fix exhaustive deps, a11y label associations, array index keys, empty CSS blocks, unused vars, type annotations - MedDetail modal: fix intake schedule bell icons not rendering (use unified intake source with fallback), place bell inline after person name - MedDetail modal: revert schedule rows from grid to flexbox layout Closes #264
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/* biome-ignore-all lint/correctness/useExhaustiveDependencies: auth refresh callbacks intentionally coordinate via refs/guards */
|
||||
import { createContext, type ReactNode, useCallback, useContext, useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { log } from "../utils/logger";
|
||||
@@ -70,7 +71,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
initialFetchDone.current = true;
|
||||
fetchAuthState();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [fetchAuthState]);
|
||||
|
||||
// Proactively refresh token every 10 minutes to prevent expiration
|
||||
useEffect(() => {
|
||||
@@ -89,7 +90,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
|
||||
return () => clearInterval(refreshInterval);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [user, authState?.authEnabled]);
|
||||
}, [user, authState?.authEnabled, refreshUser, tryRefreshToken]);
|
||||
|
||||
async function fetchAuthState(retryCount = 0) {
|
||||
const maxRetries = 3;
|
||||
|
||||
@@ -31,7 +31,15 @@ export function Lightbox({ src, alt, onClose }: LightboxProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="lightbox-overlay" onClick={handleOverlayClick}>
|
||||
<div
|
||||
className="lightbox-overlay"
|
||||
onClick={handleOverlayClick}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="lightbox-container">
|
||||
<button className="lightbox-close" onClick={onClose}>
|
||||
×
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* 1. Context mode: Uses useAppContext() for all state (when no props provided)
|
||||
* 2. Props mode: Accepts all required data as props (for gradual adoption)
|
||||
*/
|
||||
/* biome-ignore-all lint/a11y/noLabelWithoutControl: modal uses label-styled wrappers with custom interactive rows */
|
||||
/* biome-ignore-all lint/style/noNestedTernary: stock/preview rendering keeps explicit branch mapping */
|
||||
|
||||
import { Bell, Calendar, ClipboardList, FilePenLine, Minus, NotebookPen, Pencil, Plus, X } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
@@ -474,7 +476,7 @@ export function MedDetailModal({
|
||||
const rawFull = raw === "" ? 0 : Math.max(0, parseStockInput(raw));
|
||||
const rawPartial = Math.max(0, parseStockInput(editStockPartialInput));
|
||||
const rawLoose = Math.max(0, parseStockInput(editStockLooseInput));
|
||||
const rawTotal = rawFull * selectedMed.pillsPerBlister + rawPartial + rawLoose;
|
||||
const _rawTotal = rawFull * selectedMed.pillsPerBlister + rawPartial + rawLoose;
|
||||
setEditStockFullInput(raw);
|
||||
const normalized = normalizeBlisterStock(rawFull, rawPartial, rawLoose);
|
||||
onEditStockFullBlistersChange(normalized.full);
|
||||
@@ -503,7 +505,7 @@ export function MedDetailModal({
|
||||
const rawFull = Math.max(0, parseStockInput(editStockFullInput) + delta);
|
||||
const rawPartial = Math.max(0, parseStockInput(editStockPartialInput));
|
||||
const rawLoose = Math.max(0, parseStockInput(editStockLooseInput));
|
||||
const rawTotal = rawFull * selectedMed.pillsPerBlister + rawPartial + rawLoose;
|
||||
const _rawTotal = rawFull * selectedMed.pillsPerBlister + rawPartial + rawLoose;
|
||||
const normalized = normalizeBlisterStock(rawFull, rawPartial, rawLoose);
|
||||
onEditStockFullBlistersChange(normalized.full);
|
||||
onEditStockPartialBlisterPillsChange(normalized.partial);
|
||||
@@ -560,7 +562,7 @@ export function MedDetailModal({
|
||||
const nextPartial = Math.max(0, parseStockInput(editStockPartialInput) + delta);
|
||||
const nextFull = Math.max(0, parseStockInput(editStockFullInput));
|
||||
const nextLoose = Math.max(0, parseStockInput(editStockLooseInput));
|
||||
const rawTotal = nextFull * selectedMed.pillsPerBlister + nextPartial + nextLoose;
|
||||
const _rawTotal = nextFull * selectedMed.pillsPerBlister + nextPartial + nextLoose;
|
||||
const normalized = normalizeBlisterStock(nextFull, nextPartial, nextLoose);
|
||||
onEditStockFullBlistersChange(normalized.full);
|
||||
onEditStockPartialBlisterPillsChange(normalized.partial);
|
||||
@@ -815,35 +817,49 @@ export function MedDetailModal({
|
||||
)}
|
||||
</h3>
|
||||
<div className="med-detail-schedules">
|
||||
{selectedMed.blisters.map((blister, idx) => {
|
||||
// When using new intakes format with per-intake takenBy,
|
||||
// each intake already represents one person's dose — don't multiply.
|
||||
// For legacy intakes (no per-intake takenBy), multiply by personCount.
|
||||
const intake = selectedMed.intakes?.[idx];
|
||||
const hasPerIntakeTakenBy = !!intake?.takenBy;
|
||||
const personCount = hasPerIntakeTakenBy ? 1 : Math.max(1, selectedMed.takenBy?.length || 1);
|
||||
const totalUsage = blister.usage * personCount;
|
||||
{(selectedMed.intakes && selectedMed.intakes.length > 0
|
||||
? selectedMed.intakes
|
||||
: selectedMed.blisters.map((blister) => ({
|
||||
usage: blister.usage,
|
||||
every: blister.every,
|
||||
start: blister.start,
|
||||
takenBy: null,
|
||||
intakeRemindersEnabled: selectedMed.intakeRemindersEnabled ?? false,
|
||||
}))
|
||||
).map((intake, idx) => {
|
||||
const hasPerIntakeTakenBy = !!intake.takenBy;
|
||||
const personCount = Math.max(1, selectedMed.takenBy?.length ?? 0);
|
||||
const totalUsage = hasPerIntakeTakenBy ? intake.usage : intake.usage * personCount;
|
||||
const showIntakeBell = intake.intakeRemindersEnabled ?? selectedMed.intakeRemindersEnabled ?? false;
|
||||
|
||||
return (
|
||||
<div key={idx} className="med-schedule-item">
|
||||
<div key={`${intake.start}-${intake.usage}-${intake.every}-${idx}`} className="med-schedule-item">
|
||||
<span className="med-schedule-usage">
|
||||
{totalUsage} {totalUsage !== 1 ? t("common.pills") : t("common.pill")}
|
||||
{selectedMed.pillWeightMg &&
|
||||
` (${totalUsage * selectedMed.pillWeightMg} ${selectedMed.doseUnit ?? "mg"})`}
|
||||
</span>
|
||||
<span className="med-schedule-freq">
|
||||
{blister.every === 1 ? t("common.daily") : t("common.everyNDays", { count: blister.every })}
|
||||
{intake.every === 1 ? t("common.daily") : t("common.everyNDays", { count: intake.every })}
|
||||
</span>
|
||||
{hasPerIntakeTakenBy && intake.takenBy && (
|
||||
<span className="med-schedule-person">{intake.takenBy}</span>
|
||||
{hasPerIntakeTakenBy && (
|
||||
<span className="med-schedule-person">
|
||||
{intake.takenBy}
|
||||
{showIntakeBell && (
|
||||
<span className="med-schedule-bell" role="img" aria-label={t("tooltips.intakeReminders")}>
|
||||
<Bell size={13} aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{intake?.intakeRemindersEnabled && (
|
||||
{!hasPerIntakeTakenBy && showIntakeBell && (
|
||||
<span className="med-schedule-bell" role="img" aria-label={t("tooltips.intakeReminders")}>
|
||||
<Bell size={13} aria-hidden="true" />
|
||||
</span>
|
||||
)}
|
||||
<span className="med-schedule-time">
|
||||
{t("modal.at")}{" "}
|
||||
{new Date(blister.start).toLocaleTimeString(getSystemLocale(i18n.language), {
|
||||
{new Date(intake.start).toLocaleTimeString(getSystemLocale(i18n.language), {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
})}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Handles new medication creation and editing existing medications
|
||||
*/
|
||||
|
||||
/* biome-ignore-all lint/a11y/noLabelWithoutControl: modal uses custom DateInput and static value fields */
|
||||
import { Bell, Minus, Plus, Trash2 } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -91,9 +92,9 @@ export function MobileEditModal({
|
||||
onAddTakenByPerson,
|
||||
onRemoveTakenByPerson,
|
||||
onTakenByKeyDown,
|
||||
onSetBlisterValue,
|
||||
onAddBlister,
|
||||
onRemoveBlister,
|
||||
_onSetBlisterValue,
|
||||
_onAddBlister,
|
||||
_onRemoveBlister,
|
||||
onSetIntakeValue,
|
||||
onAddIntake,
|
||||
onRemoveIntake,
|
||||
@@ -639,7 +640,10 @@ export function MobileEditModal({
|
||||
)}
|
||||
</div>
|
||||
{form.intakes.map((intake, idx) => (
|
||||
<div key={idx} className="blister-row">
|
||||
<div
|
||||
key={`${intake.startDate}-${intake.startTime}-${intake.usage}-${intake.every}-${intake.takenBy ?? ""}`}
|
||||
className="blister-row"
|
||||
>
|
||||
<label className="compact">
|
||||
<span>{t("form.blisters.usage")}</span>
|
||||
<input
|
||||
|
||||
@@ -124,8 +124,12 @@ export function ShareDialog({
|
||||
return (
|
||||
<div className="share-dialog-form">
|
||||
<div className="form-group">
|
||||
<label>{t("share.selectPerson")}</label>
|
||||
<select value={shareSelectedPerson} onChange={(e) => onShareSelectedPersonChange(e.target.value)}>
|
||||
<label htmlFor="share-person-select">{t("share.selectPerson")}</label>
|
||||
<select
|
||||
id="share-person-select"
|
||||
value={shareSelectedPerson}
|
||||
onChange={(e) => onShareSelectedPersonChange(e.target.value)}
|
||||
>
|
||||
{sharePeople.map((person) => (
|
||||
<option key={person} value={person}>
|
||||
{person}
|
||||
@@ -135,8 +139,12 @@ export function ShareDialog({
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>{t("share.selectPeriod")}</label>
|
||||
<select value={shareSelectedDays} onChange={(e) => onShareSelectedDaysChange(Number(e.target.value))}>
|
||||
<label htmlFor="share-period-select">{t("share.selectPeriod")}</label>
|
||||
<select
|
||||
id="share-period-select"
|
||||
value={shareSelectedDays}
|
||||
onChange={(e) => onShareSelectedDaysChange(Number(e.target.value))}
|
||||
>
|
||||
<option value={30}>{t("dashboard.schedules.1month")}</option>
|
||||
<option value={90}>{t("dashboard.schedules.3months")}</option>
|
||||
<option value={180}>{t("dashboard.schedules.6months")}</option>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// =============================================================================
|
||||
// SharedSchedule Component - Public view for shared schedules
|
||||
// =============================================================================
|
||||
/* biome-ignore-all lint/style/noNestedTernary: rendering branches are intentionally explicit in schedule UI */
|
||||
/* biome-ignore-all lint/correctness/useExhaustiveDependencies: modal and helper callbacks are stable at runtime */
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -98,13 +98,14 @@ export function UserFilterModal({
|
||||
{med.genericName && <span className="user-med-generic">{med.genericName}</span>}
|
||||
{personIntakes.length > 0 && (
|
||||
<div className="user-med-intakes">
|
||||
{personIntakes.map((intake, idx) => {
|
||||
{personIntakes.map((intake) => {
|
||||
const timeStr = new Date(intake.start).toLocaleTimeString(getSystemLocale(i18n.language), {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
const intakeKey = `${intake.start}-${intake.usage}-${intake.every}-${intake.takenBy ?? ""}`;
|
||||
return (
|
||||
<span key={idx} className="user-med-intake-item">
|
||||
<span key={intakeKey} className="user-med-intake-item">
|
||||
{intake.usage} {intake.usage !== 1 ? t("common.pills") : t("common.pill")}
|
||||
{med.pillWeightMg != null &&
|
||||
` (${intake.usage * med.pillWeightMg} ${med.doseUnit ?? "mg"})`}{" "}
|
||||
|
||||
Reference in New Issue
Block a user