mirror of
https://github.com/temetro/temetro.git
synced 2026-08-26 02:17:22 +00:00
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:
@@ -8,7 +8,7 @@ import {
|
||||
Trash2,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||
@@ -115,12 +115,19 @@ export function EmployeeDetailDialog({
|
||||
const [confirmPw, setConfirmPw] = useState("");
|
||||
const [savingPw, setSavingPw] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Re-seed when a different member (or their role/specialty) is shown, and
|
||||
// always clear the password fields so they can't carry across. Adjusted
|
||||
// during render rather than in an effect, so the dialog never paints another
|
||||
// member's values — and a typed password never survives a switch.
|
||||
const seed = `${member?.id ?? ""}|${member?.role ?? ""}|${member?.specialty ?? ""}`;
|
||||
const [prevSeed, setPrevSeed] = useState(seed);
|
||||
if (prevSeed !== seed) {
|
||||
setPrevSeed(seed);
|
||||
setRole(member?.role ?? "");
|
||||
setSpecialty(member?.specialty ?? "");
|
||||
setNewPw("");
|
||||
setConfirmPw("");
|
||||
}, [member?.id, member?.role, member?.specialty]);
|
||||
}
|
||||
|
||||
const summary = rolePermissionSummary(member?.role);
|
||||
const secondary = member?.username ? `@${member.username}` : member?.email;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Info, UserPlus } from "lucide-react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { AddStaffDialog } from "@/components/settings/add-staff-dialog";
|
||||
@@ -64,36 +64,47 @@ export function CareTeamPanel({
|
||||
const [pendingRemove, setPendingRemove] = useState<StaffMember | null>(null);
|
||||
const [removing, setRemoving] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const data = await apiFetch<StaffMember[]>("/api/staff");
|
||||
setMembers(data);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError(
|
||||
err instanceof Error ? err.message : t("settings.careTeam.loadError"),
|
||||
);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
// The effect owns the fetch; `reload()` just bumps a key to re-run it. That
|
||||
// keeps every setState inside a promise callback rather than running
|
||||
// synchronously when the effect fires, and it drops the exhaustive-deps
|
||||
// suppression the old useCallback needed.
|
||||
const [reloadKey, setReloadKey] = useState(0);
|
||||
const reload = () => setReloadKey((k) => k + 1);
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [load]);
|
||||
let active = true;
|
||||
apiFetch<StaffMember[]>("/api/staff")
|
||||
.then((data) => {
|
||||
if (!active) return;
|
||||
setMembers(data);
|
||||
setError(null);
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
if (!active) return;
|
||||
setError(
|
||||
err instanceof Error ? err.message : t("settings.careTeam.loadError"),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
if (active) setLoading(false);
|
||||
});
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
// `t` is intentionally omitted: re-fetching the staff list on a language
|
||||
// change would be pointless, and the message is only read on failure.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [reloadKey]);
|
||||
|
||||
// Deep-link: open a specific member (e.g. from a password-reset system card).
|
||||
const appliedDeepLink = useRef(false);
|
||||
useEffect(() => {
|
||||
if (appliedDeepLink.current || !initialMemberId || members.length === 0)
|
||||
return;
|
||||
// Resolved during render once the list arrives, so the member's dialog opens
|
||||
// on the same paint the list does.
|
||||
const [appliedDeepLink, setAppliedDeepLink] = useState(false);
|
||||
if (!appliedDeepLink && initialMemberId && members.length > 0) {
|
||||
setAppliedDeepLink(true);
|
||||
const target = members.find((m) => m.userId === initialMemberId);
|
||||
if (target) {
|
||||
setSelected(target);
|
||||
appliedDeepLink.current = true;
|
||||
}
|
||||
}, [initialMemberId, members]);
|
||||
if (target) setSelected(target);
|
||||
}
|
||||
|
||||
const myRole = members.find((m) => m.userId === session?.user?.id)?.role;
|
||||
const canManage = myRole === "owner" || myRole === "admin";
|
||||
@@ -119,7 +130,7 @@ export function CareTeamPanel({
|
||||
}),
|
||||
);
|
||||
setPendingRemove(null);
|
||||
void load();
|
||||
reload();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -211,7 +222,7 @@ export function CareTeamPanel({
|
||||
|
||||
{canManage && (
|
||||
<AddStaffDialog
|
||||
onCreated={() => void load()}
|
||||
onCreated={reload}
|
||||
onOpenChange={setAdding}
|
||||
open={adding}
|
||||
/>
|
||||
@@ -225,7 +236,7 @@ export function CareTeamPanel({
|
||||
selected?.role !== "owner"
|
||||
}
|
||||
member={selected}
|
||||
onChanged={() => void load()}
|
||||
onChanged={reload}
|
||||
onOpenChange={(o) => !o && setSelected(null)}
|
||||
onRemove={(m) => {
|
||||
setSelected(null);
|
||||
|
||||
@@ -39,12 +39,18 @@ export function PatientPortalSection() {
|
||||
const origin = typeof window !== "undefined" ? window.location.origin : "";
|
||||
const portalUrl = slug ? `${origin}/portal/${slug}` : "";
|
||||
|
||||
// Drop the previous clinic's QR as soon as the slug changes, rather than
|
||||
// leaving it on screen until the new one arrives. Adjusted during render, so
|
||||
// switching clinics never shows the old clinic's pairing code.
|
||||
const [prevSlug, setPrevSlug] = useState(slug);
|
||||
if (prevSlug !== slug) {
|
||||
setPrevSlug(slug);
|
||||
setQrUri("");
|
||||
}
|
||||
|
||||
// Fetch the relay-based pairing descriptor for the QR (non-secret).
|
||||
useEffect(() => {
|
||||
if (!slug) {
|
||||
setQrUri("");
|
||||
return;
|
||||
}
|
||||
if (!slug) return;
|
||||
let active = true;
|
||||
getPortalLink(slug)
|
||||
.then((link) => {
|
||||
|
||||
@@ -113,13 +113,17 @@ export function ProfilePanel() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Seed the display name from the session once it loads.
|
||||
useEffect(() => {
|
||||
// Seed the display name from the session once it loads. Adjusted during
|
||||
// render so the field isn't briefly empty after the session resolves. Still
|
||||
// `prev ||`, so it never clobbers something the clinician has typed.
|
||||
const [prevUserName, setPrevUserName] = useState(user?.name);
|
||||
if (prevUserName !== user?.name) {
|
||||
setPrevUserName(user?.name);
|
||||
if (user?.name) {
|
||||
setName((prev) => prev || user.name);
|
||||
setBaselineName((prev) => prev || user.name);
|
||||
}
|
||||
}, [user?.name]);
|
||||
}
|
||||
|
||||
const setPref = (key: string, value: boolean | string) =>
|
||||
setPrefs((prev) => ({ ...prev, [key]: value }));
|
||||
|
||||
Reference in New Issue
Block a user