Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 17 additions & 15 deletions src/app/global-error.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect } from "react";
import { useEffect, useRef } from "react";

/**
* Last-resort boundary for the App Router. Unlike `app/error.tsx`, this replaces
Expand All @@ -11,8 +11,10 @@ import { useEffect } from "react";
* still renders correctly when the styling/theming system is exactly what failed.
*/
export default function GlobalError({ error, reset }: { error: Error & { digest?: string }; reset: () => void }) {
const headingRef = useRef<HTMLHeadingElement>(null);
useEffect(() => {
console.error("Fatal error captured by global-error boundary:", error);
headingRef.current?.focus();
}, [error]);

return (
Expand All @@ -25,8 +27,8 @@ export default function GlobalError({ error, reset }: { error: Error & { digest?
alignItems: "center",
justifyContent: "center",
padding: "1rem",
backgroundColor: "#f4f5f7",
color: "#1a1c1e",
backgroundColor: "Canvas",
color: "CanvasText",
fontFamily:
'system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"',
}}
Expand All @@ -37,27 +39,27 @@ export default function GlobalError({ error, reset }: { error: Error & { digest?
maxWidth: "28rem",
boxSizing: "border-box",
borderRadius: "1rem",
border: "1px solid #e2e4e8",
backgroundColor: "#ffffff",
border: "1px solid CanvasText",
backgroundColor: "Canvas",
padding: "1.5rem",
textAlign: "center",
boxShadow: "0 10px 30px rgba(15, 23, 42, 0.12)",
boxShadow: "0 10px 30px color-mix(in srgb, CanvasText 12%, transparent)",
}}
>
<h1 style={{ margin: "0 0 0.5rem", fontSize: "1.125rem", fontWeight: 600 }}>Something went wrong</h1>
<p style={{ margin: "0 0 1.25rem", fontSize: "0.875rem", lineHeight: 1.6, color: "#5b6069" }}>
<h1 ref={headingRef} tabIndex={-1} role="alert" style={{ margin: "0 0 0.5rem", fontSize: "1.125rem", fontWeight: 600, outline: "none" }}>Something went wrong</h1>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== file list ==\n'
git ls-files 'src/app/global-error.tsx' 'src/components/route-error-boundary.tsx' 'src/**/error*.tsx' 'src/**/error*.ts' 'src/**/global-error.tsx'

printf '\n== outline global-error ==\n'
ast-grep outline src/app/global-error.tsx --view expanded || true

printf '\n== outline route-error-boundary ==\n'
ast-grep outline src/components/route-error-boundary.tsx --view expanded || true

printf '\n== relevant snippets ==\n'
sed -n '1,140p' src/app/global-error.tsx
printf '\n---\n'
sed -n '1,180p' src/components/route-error-boundary.tsx

Repository: BigSimmo/Database

Length of output: 8716


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== search for focus styles in error boundaries ==\n'
rg -n "focus:outline-none|outline:\s*\"none\"|tabIndex=\{-1\}|role=\"alert\"|headingRef" src/app src/components src -g '!**/node_modules/**'

printf '\n== search for visible focus token usage ==\n'
rg -n "var\(--focus\)|focus-visible|focus:outline|outline-offset|outline-" src/app src/components src -g '!**/node_modules/**' | sed -n '1,200p'

Repository: BigSimmo/Database

Length of output: 50373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
for p in [Path('src/app/global-error.tsx'), Path('src/components/route-error-boundary.tsx')]:
    print(f"\n## {p}")
    text = p.read_text()
    for needle in ['outline: "none"', 'focus:outline-none', 'tabIndex={-1}', 'role="alert"']:
        print(needle, text.find(needle))
PY

Repository: BigSimmo/Database

Length of output: 386


Restore a visible focus style on the error headings.

Both boundaries move focus to the <h1>, but outline: "none" / focus:outline-none removes the only visible focus indicator. Add a visible outline instead.

  • src/app/global-error.tsx#L49
  • src/components/route-error-boundary.tsx#L59-L61
📍 Affects 2 files
  • src/app/global-error.tsx#L49-L49 (this comment)
  • src/components/route-error-boundary.tsx#L59-L61
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/global-error.tsx` at line 49, Restore a visible focus indicator on
the error headings by replacing the outline removal in src/app/global-error.tsx
lines 49-49 with a visible outline style, and update the focused heading in
src/components/route-error-boundary.tsx lines 59-61 to remove focus:outline-none
and provide an equivalent visible outline. Keep the existing heading focus
behavior unchanged.

<p style={{ margin: "0 0 1.25rem", fontSize: "0.875rem", lineHeight: 1.6 }}>
The application failed to load. Please try again, or reload the page if the problem persists.
</p>
{error.digest && (
<div
style={{
margin: "0 0 1.25rem",
borderRadius: "0.5rem",
backgroundColor: "#f4f5f7",
backgroundColor: "Canvas",
padding: "0.5rem",
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
fontSize: "0.75rem",
color: "#5b6069",
color: "CanvasText",
wordBreak: "break-all",
}}
>
Expand All @@ -72,8 +74,8 @@ export default function GlobalError({ error, reset }: { error: Error & { digest?
cursor: "pointer",
borderRadius: "0.5rem",
border: "none",
backgroundColor: "#1a1c1e",
color: "#ffffff",
backgroundColor: "ButtonFace",
color: "ButtonText",
padding: "0.625rem 1rem",
fontSize: "0.875rem",
fontWeight: 600,
Expand All @@ -87,9 +89,9 @@ export default function GlobalError({ error, reset }: { error: Error & { digest?
style={{
cursor: "pointer",
borderRadius: "0.5rem",
border: "1px solid #e2e4e8",
backgroundColor: "#ffffff",
color: "#1a1c1e",
border: "1px solid ButtonText",
backgroundColor: "ButtonFace",
color: "ButtonText",
padding: "0.625rem 1rem",
fontSize: "0.875rem",
fontWeight: 600,
Expand Down
8 changes: 6 additions & 2 deletions src/components/route-error-boundary.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect } from "react";
import { useEffect, useRef } from "react";
import { TriangleAlert, RefreshCw } from "lucide-react";

import { cn, primaryControl } from "@/components/ui-primitives";
Expand Down Expand Up @@ -38,8 +38,10 @@ export function RouteErrorBoundary({
showReload = false,
minHeightClass = "min-h-[50vh]",
}: RouteErrorBoundaryProps) {
const headingRef = useRef<HTMLHeadingElement>(null);
useEffect(() => {
console.error(logLabel, error);
headingRef.current?.focus();
}, [error, logLabel]);

return (
Expand All @@ -54,7 +56,9 @@ export function RouteErrorBoundary({
<TriangleAlert aria-hidden="true" className="h-6 w-6" />
</div>

<h1 className="mt-4 text-lg font-semibold tracking-tight text-[color:var(--text-heading)]">{title}</h1>
<h1 ref={headingRef} tabIndex={-1} className="mt-4 text-lg font-semibold tracking-tight text-[color:var(--text-heading)] focus:outline-none">
{title}
</h1>

<p className="mt-2 text-sm leading-relaxed text-[color:var(--text-muted)]">{description}</p>

Expand Down
5 changes: 4 additions & 1 deletion src/components/therapy-compass/bindings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type TcBindings = {
// ---- data -----------------------------------------------------------
loading: boolean;
error: string | null;
retryData: () => void;
therapies: Therapy[];
unreviewedTherapies: Therapy[];
reviewCount: number;
Expand Down Expand Up @@ -207,7 +208,7 @@ export function TcProvider({
initialQuery?: string;
autoRunSearch?: boolean;
}) {
const { data, loading, error } = useTherapyData();
const { data, loading, error, retry } = useTherapyData();
const therapies = useMemo(() => data?.therapies ?? [], [data]);
const pathways = useMemo(() => data?.pathways ?? [], [data]);

Expand Down Expand Up @@ -271,6 +272,7 @@ export function TcProvider({
return {
loading,
error,
retryData: retry,
therapies,
unreviewedTherapies,
reviewCount: unreviewedTherapies.length,
Expand Down Expand Up @@ -429,6 +431,7 @@ export function TcProvider({
}, [
loading,
error,
retry,
data,
therapies,
unreviewedTherapies,
Expand Down
15 changes: 11 additions & 4 deletions src/components/therapy-compass/data/use-therapy-data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";

import type { Pathway, ReferenceData, Therapy, TherapyDataset } from "./types";

Expand Down Expand Up @@ -32,10 +32,17 @@ export type TherapyDataState = {
data: TherapyDataset | null;
loading: boolean;
error: string | null;
retry: () => void;
};

export function useTherapyData(): TherapyDataState {
const [state, setState] = useState<TherapyDataState>({ data: null, loading: true, error: null });
const [state, setState] = useState<Omit<TherapyDataState, "retry">>({ data: null, loading: true, error: null });
const [attempt, setAttempt] = useState(0);
const retry = useCallback(() => {
cache = null;
setState({ data: null, loading: true, error: null });
setAttempt((value) => value + 1);
}, []);
Comment on lines +41 to +45

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -P '\buseTherapyData\s*\(' src

Repository: BigSimmo/Database

Length of output: 375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== use-therapy-data.ts ==\n'
cat -n src/components/therapy-compass/data/use-therapy-data.ts | sed -n '1,240p'

printf '\n== bindings.tsx (relevant range) ==\n'
cat -n src/components/therapy-compass/bindings.tsx | sed -n '180,250p'

printf '\n== all useTherapyData references ==\n'
rg -n -P '\buseTherapyData\s*\(' src

Repository: BigSimmo/Database

Length of output: 7157


🏁 Script executed:

#!/bin/bash
set -euo pipefail
cat -n src/components/therapy-compass/data/use-therapy-data.ts | sed -n '1,220p'

Repository: BigSimmo/Database

Length of output: 2786


Guard cache before clearing it. retry can start a new load while a previous promise is still pending, and the older rejection handler currently does cache = null unconditionally. That can wipe out the newer in-flight promise and force an unnecessary refetch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/therapy-compass/data/use-therapy-data.ts` around lines 41 -
45, Update the retry and promise rejection flow around retry and cache handling
so an older request only clears cache when it still owns the cached promise;
guard the cache reset by comparing the relevant promise/reference before
assigning null, preserving newer in-flight requests.


useEffect(() => {
let active = true;
Expand All @@ -53,7 +60,7 @@ export function useTherapyData(): TherapyDataState {
return () => {
active = false;
};
}, []);
}, [attempt]);

return state;
return { ...state, retry };
}
6 changes: 4 additions & 2 deletions src/components/therapy-compass/therapy-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ export function ResultCard({ therapy }: { therapy: Therapy }) {
<button
type="button"
className="tc-btn"
title="Favourite"
disabled
title="Favourites are not available yet"
aria-label="Favourites are coming soon"
style={s(
`display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border:1px solid var(--border);border-radius:9px;background:var(--surface);color:var(--text-soft);cursor:pointer;`,
`display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;border:1px solid var(--border);border-radius:9px;background:var(--surface);color:var(--text-soft);cursor:not-allowed;opacity:.65;`,
)}
>
<HeartIcon size={17} />
Expand Down
30 changes: 21 additions & 9 deletions src/components/therapy-compass/therapy-compass-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,27 @@ function TherapyCompassShell() {
>
<TherapyCompassNav />
<main className="tc-main" style={s(`min-width:0;padding:32px 40px 40px;`)}>
{b.isHome && <HomeScreen />}
{b.isSearch && <SearchScreen />}
{b.isDetail && <DetailScreen />}
{b.isCompare && <CompareScreen />}
{b.isRecommend && <RecommendScreen />}
{b.isPathways && <PathwaysScreen />}
{b.isBrief && <BriefScreen />}
{b.isSheets && <SheetsScreen />}
{b.isOther && <OtherScreen />}
{b.error ? (
<section role="alert" aria-live="assertive" style={s(`max-width:42rem;margin:40px auto;padding:24px;border:1px solid var(--danger);border-radius:16px;background:var(--danger-soft);`)}>
<h1 style={s(`margin:0 0 8px;color:var(--text-heading);font-size:20px;`)}>Therapy Compass could not load</h1>
<p style={s(`margin:0 0 16px;color:var(--text-muted);line-height:1.5;`)}>The therapy catalogue is unavailable. No results are being shown as a substitute.</p>
<button type="button" className="tc-btn" onClick={b.retryData} style={s(`padding:10px 14px;border:0;border-radius:8px;background:var(--clinical-accent);color:var(--clinical-accent-contrast);font-weight:650;`)}>
Retry
</button>
</section>
) : (
Comment on lines +47 to +55

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Preserve focus when Retry starts loading.

Clicking Retry clears b.error, which immediately unmounts the focused button. Focus then falls back to the document/body, so keyboard and screen-reader users lose their place during recovery. Move focus to a stable loading/content container or keep this panel mounted and disable/update the button while the retry is in progress.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/therapy-compass/therapy-compass-page.tsx` around lines 47 -
55, Update the error/retry flow around b.retryData so focus is preserved when
retry clears b.error and unmounts the Retry button. Move focus to a stable
loading or content container after retry begins, or keep the error panel mounted
with the button disabled and updated during loading; ensure keyboard and
screen-reader users retain a logical focus position throughout recovery.

<>
{b.isHome && <HomeScreen />}
{b.isSearch && <SearchScreen />}
{b.isDetail && <DetailScreen />}
{b.isCompare && <CompareScreen />}
{b.isRecommend && <RecommendScreen />}
{b.isPathways && <PathwaysScreen />}
{b.isBrief && <BriefScreen />}
{b.isSheets && <SheetsScreen />}
{b.isOther && <OtherScreen />}
</>
)}
<TherapyCompassFooter />
</main>
</div>
Expand Down
Loading