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
@@ -76,7 +76,6 @@ function VideoTile({
speaking ? "border-success" : "border-transparent",
)}
>
{/* eslint-disable-next-line jsx-a11y/media-has-caption */}
<video
autoPlay
className={cn("size-full object-cover", !showVideo && "invisible")}
+11 -10
View File
@@ -59,7 +59,6 @@ export function MeetingsView() {
// ?with=<userId> from the Messages inbox "call" button — open the scheduler
// pre-targeted at that person so the user can connect with them.
const deepLinkWith = searchParams.get("with");
const openedDeepLink = useRef<string | null>(null);
const openedWith = useRef<string | null>(null);
const [tab, setTab] = useState<Tab>("rooms");
@@ -104,16 +103,18 @@ export function MeetingsView() {
};
}, []);
// Auto-join a room deep-linked from an invite (?room=).
useEffect(() => {
if (!deepLinkRoom || rooms.length === 0) return;
if (openedDeepLink.current === deepLinkRoom) return;
// Auto-join a room deep-linked from an invite (?room=). Resolved during
// render once the room list arrives, so the room opens on the same paint the
// list does instead of flashing the default tab first.
const [openedDeepLink, setOpenedDeepLink] = useState<string | null>(null);
if (deepLinkRoom && rooms.length > 0 && openedDeepLink !== deepLinkRoom) {
const room = rooms.find((r) => r.id === deepLinkRoom);
if (!room) return;
openedDeepLink.current = deepLinkRoom;
setTab("rooms");
setActiveRoom(room);
}, [deepLinkRoom, rooms]);
if (room) {
setOpenedDeepLink(deepLinkRoom);
setTab("rooms");
setActiveRoom(room);
}
}
// Open the scheduler pre-targeted at a person (?with=) from the inbox.
useEffect(() => {
@@ -54,12 +54,21 @@ export function ScheduleMeetingDialog({
const [picked, setPicked] = useState<Set<string>>(new Set());
const [saving, setSaving] = useState(false);
// Re-seed the form each time the dialog opens. Adjusted during render so it
// never paints the previous meeting's title or participants.
const [prevOpen, setPrevOpen] = useState(false);
if (prevOpen !== open) {
setPrevOpen(open);
if (open) {
setTitle("");
setDate(defaultDate ?? "");
setTime("09:00");
setPicked(new Set(defaultParticipants ?? []));
}
}
useEffect(() => {
if (!open) return;
setTitle("");
setDate(defaultDate ?? "");
setTime("09:00");
setPicked(new Set(defaultParticipants ?? []));
listClinicMembers()
.then(setMembers)
.catch(() => setMembers([]));
@@ -8,11 +8,17 @@ import { useEffect, useState } from "react";
export function useSpeaking(stream: MediaStream | null): boolean {
const [speaking, setSpeaking] = useState(false);
// Stop reporting "speaking" the moment the stream goes away or changes, so a
// muted tile can't keep a stale ring. Adjusted during render rather than in
// the effect below, which is only for driving the Web Audio graph.
const [prevStream, setPrevStream] = useState(stream);
if (prevStream !== stream) {
setPrevStream(stream);
setSpeaking(false);
}
useEffect(() => {
if (!stream || stream.getAudioTracks().length === 0) {
setSpeaking(false);
return;
}
if (!stream || stream.getAudioTracks().length === 0) return;
let ctx: AudioContext | null = null;
let raf = 0;
try {