frontend: clear the 99 lint errors

`npm run lint` reported 99 errors and 26 warnings across 65 files and
had presumably been failing for a while — next.config.ts sets
eslint.ignoreDuringBuilds, so the build never surfaced it.

68 were in vendored code: components/charts and components/ai-elements,
pulled from upstream registries. Re-linting those reports upstream's
style back at us, and "fixing" them means diverging and eating conflicts
on every update — ai-elements already has exactly this carve-out on the
TypeScript side via ignoreBuildErrors. Ignore both, plus the two registry
files in components/ui (carousel publishes its api from an effect; the
sidebar skeleton picks a random width).

The other 31 were ours, nearly all react-hooks/set-state-in-effect on
the same shape: an effect that re-seeds form state when a dialog opens or
a selection changes. Moved to render-phase adjustment, which is both what
React recommends and a real fix — the effect version paints one frame of
the *previous* record's values before correcting itself. Two carried
sharper bugs: the employee dialog could keep a typed password across a
switch to another member, and use-wallet-sync could carry `linked` over
to a newly-selected patient, briefly offering to push a record to
someone else's wallet.

The rest: useIsMobile and speech-support detection become
useSyncExternalStore (correct on first paint, no mount flash); refs
mirroring state are written in effects rather than during render; the
care-team fetch moves into its effect behind a reload key, dropping an
exhaustive-deps suppression.

Two effects in chat-panel keep the rule disabled with a reason. Both are
what effects are for: draining the queued-message buffer when the
transport goes idle, and resolving ?thread from the URL — the latter
mints an id with nanoid(), so moving it into render would just trade this
error for a purity one.

Also removes a dead /explore-era import and unused directives found on
the way. lint now exits 0, with the ai-elements tsc carve-out unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-07-15 20:54:37 +03:00
parent 7c92c03eed
commit e8f3ed9ffe
28 changed files with 325 additions and 136 deletions
@@ -79,8 +79,12 @@ export function ImportFromWalletDialog({
}
};
// Reset everything whenever the dialog is (re)opened.
useEffect(() => {
// Reset everything whenever the dialog is (re)opened. Adjusted during render
// so a reopened dialog never flashes the previous import's wallet number or
// state before clearing.
const [prevOpen, setPrevOpen] = useState(false);
if (prevOpen !== open) {
setPrevOpen(open);
if (open) {
setMode("number");
setWalletNumber("");
@@ -92,8 +96,10 @@ export function ImportFromWalletDialog({
setPairUri(null);
setReviewOpen(false);
}
return stopPolling;
}, [open]);
}
// Stop polling when the dialog closes or unmounts.
useEffect(() => stopPolling, [open]);
// Poll the request until the patient approves/denies on their device.
useEffect(() => {
@@ -102,14 +102,29 @@ export function PatientDetailSheet({
// Bumped on open so the editor remounts with the latest patient data.
const [editKey, setEditKey] = useState(0);
// Clear the previous chart the moment a different one is opened, so the sheet
// can't show one patient's data under another's name while the fetch is in
// flight. Adjusted during render rather than in the effects below, which are
// only for the fetching itself. Keyed on the role too, so losing clinical
// access drops the wallet state instead of leaving the "Push update" button
// lit from the previous render.
const chart = open ? `${fileNumber ?? ""}|${role ?? ""}` : null;
const [prevChart, setPrevChart] = useState<string | null>(null);
if (prevChart !== chart) {
setPrevChart(chart);
setWalletLinked(false);
if (chart) {
setStatus("loading");
setPatient(null);
setPrescriptions([]);
setAppointments([]);
setInvoices([]);
}
}
useEffect(() => {
if (!open || !fileNumber) return;
let active = true;
setStatus("loading");
setPatient(null);
setPrescriptions([]);
setAppointments([]);
setInvoices([]);
getPatient(fileNumber)
.then((data) => {
if (!active) return;
@@ -138,9 +153,10 @@ export function PatientDetailSheet({
// Whether this patient is wallet-linked (drives the "Push update" button).
// Separate from the main load so it re-checks once the role resolves without
// refetching the record. Only clinicians can push.
// refetching the record. Only clinicians can push. The reset rides on the
// same render-phase clear as the chart above, so the button can't stay lit
// from the previous patient.
useEffect(() => {
setWalletLinked(false);
if (!open || !fileNumber || !hasClinicalAccess(role)) return;
let active = true;
getWalletLink(fileNumber)
@@ -76,9 +76,10 @@ export function PatientsView() {
const [loading, setLoading] = useState(true);
const [loadError, setLoadError] = useState<string | null>(null);
// Runs once on mount, and `loading` already starts true, so there's nothing
// to flip synchronously here.
useEffect(() => {
let active = true;
setLoading(true);
listPatients()
.then((data) => {
if (!active) return;
@@ -97,6 +98,9 @@ export function PatientsView() {
return () => {
active = false;
};
// `t` intentionally omitted: it is only read to build a failure message,
// and re-fetching on a language change would be pointless.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const q = query.trim().toLowerCase();
@@ -44,10 +44,19 @@ export function TransferPatientDialog({
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
// Re-seed the form each time the dialog opens. Adjusted during render so it
// never paints the previous patient's provider.
const [prevOpen, setPrevOpen] = useState(false);
if (prevOpen !== open) {
setPrevOpen(open);
if (open) {
setProviderId(patient.primaryProviderId ?? "");
setError(null);
}
}
useEffect(() => {
if (!open) return;
setProviderId(patient.primaryProviderId ?? "");
setError(null);
let active = true;
listProviders()
.then((list) => active && setProviders(list))