Skip to content

Todos should do#14

Open
bklaing2 wants to merge 99 commits intomainfrom
todos-should-do
Open

Todos should do#14
bklaing2 wants to merge 99 commits intomainfrom
todos-should-do

Conversation

@bklaing2
Copy link
Member

@bklaing2 bklaing2 commented Feb 17, 2026

Purpose

Addresses the "Should Do" todos.

Notable updates:

  • Implements provider views for organizations and consortia
  • Updates the breadcrumbs to allow selecting other repositories/organizations within the same parent provider
  • Refreshes the UI
  • Adds better loading states

closes: #6 and #7

Approach

Open Questions and Pre-Merge TODOs

Learning

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

  • New feature (non-breaking change which adds functionality)

  • Breaking change (fix or feature that would cause existing functionality to change)

Reviewer, please remember our guidelines:

  • Be humble in the language and feedback you give, ask don't tell.
  • Consider using positive language as opposed to neutral when offering feedback. This is to avoid the negative bias that can occur with neutral language appearing negative.
  • Offer suggestions on how to improve code e.g. simplification or expanding clarity.
  • Ensure you give reasons for the changes you are proposing.

Summary by CodeRabbit

Release Notes

  • New Features

    • Dynamic resource detail pages with comprehensive metadata display
    • Enhanced search and filter capabilities for resource navigation
    • Loading indicators for improved user feedback
    • Error pages for graceful error handling
  • Style

    • Updated design system with DataCite-branded colors and typography
    • Redesigned header with logo branding
    • Improved spacing and layout consistency
    • Enhanced badge and button styling
  • Refactor

    • Restructured data fetching and routing architecture
    • Reorganized UI components for better maintainability
  • Chores

    • Updated dependencies for improved component functionality

…percentage above bar. bold distribution "Value of..." header
@bklaing2 bklaing2 self-assigned this Feb 17, 2026
@vercel
Copy link
Contributor

vercel bot commented Feb 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
puli Ready Ready Preview, Comment Feb 17, 2026 4:32pm

Request Review

@jrhoads
Copy link

jrhoads commented Feb 18, 2026

@CodeRabbit please try again

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

@jrhoads: I'll review the changes in this PR.

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

This pull request restructures the application from client-ID-based routing to resource-based routing, consolidates data fetching into a unified module, updates UI components to use new base-ui primitives, and introduces new styling with typography font changes.

Changes

Cohort / File(s) Summary
Package Dependencies
package.json
Added @base-ui/react and radix-ui dependencies for new UI component primitives.
Route Structure Replacement
src/app/[[...id]]/page.tsx, src/app/[[...id]]/layout.tsx, src/app/[[...id]]/Header.tsx, src/app/page.tsx
Replaced home page with a catch-all [[...id]] route that handles dynamic resource IDs, includes new layout and header components with breadcrumb/action-button integration, enforces ID case-normalization, and validates resources via NotFound.
Legacy Route Removal
src/app/[clientId]/page.tsx, src/app/[clientId]/layout.tsx, src/app/[clientId]/Cards.tsx, src/app/[clientId]/Header.tsx
Removed entire client-ID-based route structure, including old page, layout, header, and 19+ card components (Creators, Contributors, ResourceType, etc.).
Data Fetching Consolidation
src/data/fetch.ts
Introduced unified data-fetching module exporting fetchResource, fetchDois, and hooks (useResource, useDois) alongside 12 specialized fetch/hook pairs for metadata fields (creators, contributors, dates, titles, etc.) with shared formatting patterns.
Legacy Data Fetch Removal
src/data/fetchCreators.ts, src/data/fetchContributors.ts, src/data/fetchDates.ts, src/data/fetchDescriptions.ts, src/data/fetchFundingReferences.ts, src/data/fetchOther.ts, src/data/fetchOverview.ts, src/data/fetchPublisher.ts, src/data/fetchRelatedIdentifiers.ts, src/data/fetchResourceType.ts, src/data/fetchRights.ts, src/data/fetchSubjects.ts, src/data/fetchTitles.ts
Removed 13 individual data-fetch modules in favor of consolidated fetch.ts exports.
Component Migration
src/components/cards/Cards.tsx
Introduced new client-side Cards module with 19 data-driven chart components (Creators, Contributors, RelatedIdentifiers, etc.) using shared ChartsCard, PresentChart, DistributionChart, and skeleton loading patterns.
Combobox Primitive Rewrite
src/components/ui/combobox.tsx
Rewrote Combobox from Command/Popover pattern to modular @base-ui/react primitives with 16+ new composable subcomponents (ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxChips, etc.) and expanded public API surface.
New UI Primitives
src/components/ui/input-group.tsx, src/components/ui/item.tsx, src/components/ui/skeleton.tsx, src/components/ui/spinner.tsx, src/components/ui/textarea.tsx, src/components/datacite/Headings.tsx
Added six new UI modules: InputGroup with variants (InputGroupAddon, InputGroupButton, InputGroupInput, etc.), Item composition system (ItemMedia, ItemContent, ItemTitle, etc.), Skeleton and Spinner loaders, Textarea, and H1-H6 heading components with font integration.
Chart Component Updates
src/components/PresentBar.tsx, src/components/PresentChart.tsx, src/components/RadialChart.tsx, src/components/DistributionChart.tsx, src/components/ResourceTypesChart.tsx, src/components/DoiRegistrationsChart.tsx
Updated chart components to use BAR color constants, simplified charting configs, added tooltip support in DoiRegistrationsChart, reduced visual clutter (hid axes), and refactored PresentBar layout with percentage display.
UI Component Refactors
src/components/Breadcrumbs.tsx, src/components/ActionButtons.tsx, src/components/cards/ChartsCard.tsx, src/components/cards/OverviewCard.tsx, src/components/PresentChart.tsx
Rewired Breadcrumbs to derive hierarchy from useResource() instead of props, replaced ActionButtons hard-coded logic with Combobox-driven filters and URLSearchParams navigation, updated ChartsCard props to accept ReactNode for title/description/present, switched OverviewCard to use useDois() with isFetching state.
Badge and Badge Component Changes
src/components/Badges.tsx, src/components/HighImpactBadge.tsx, src/components/ui/badge.tsx
Introduced new Badges.tsx with ResourceBadge and HighImpactBadge components; removed old standalone HighImpactBadge.tsx; updated badge.tsx to export BadgeProps type and variants alongside Badge component.
Utility Function Updates
src/components/LearnMore.tsx, src/components/LoadingIndicator.tsx, src/components/Skeleton.tsx
Extended LearnMore to accept optional custom text prop, added LoadingIndicator using useIsFetching() from @tanstack/react-query, introduced OverviewCardSkeleton and ChartsCardSkeleton placeholder components.
Hook API Overhaul
src/hooks.ts
Replaced useClientId() with useId(), refactored useCreateQuery<R>useQueryId<R> accepting id instead of clientId, added new useQueryResource<R> for resource-based fetching, rewired useFilters() to read from SEARCH_PARAMETERS constants.
Type System Expansion
src/types.ts
Introduced generic API schema (Relationship, Relationships, ApiData, ApiResponse, ApiDois) replacing concrete ApiResponse shape, added Resource type for hierarchical resource modeling (id, type, subtype, name, children, parent).
Constants and Utilities
src/constants.ts, src/util.ts, src/lib/fonts.ts
Added API_URL_DATACITE, PALETTE_RESOURCE_TYPE, CHART.bar styling, SEARCH_PARAMETERS/FILTERS configuration; introduced buildInitialData and isClient utilities; refactored fetchFields to use Resource objects; added dmSans and barlow font exports.
Styling and Layout
src/app/globals.css, src/app/layout.tsx, src/components/ui/card.tsx, src/components/ui/button.tsx, src/components/ui/chart.tsx, src/components/ui/input.tsx
Updated global CSS variables (new card-title, DataCite color tokens, shadow-sm); switched layout font from Source_Sans_3 to barlow; added LoadingIndicator and Usersnap script; expanded button size variants (xs, icon-xs); refined chart and card styling; adjusted input import style.
Error Handling
src/app/error.tsx
Added new client-side error page component displaying error messages in an h2 element.
Header Refactor
src/app/Header.tsx
Replaced static text-based header with logo-image header using Next.js Image and Link components, wrapping logo in H1 for semantics.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch todos-should-do

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/ActionButtons.tsx (1)

209-234: ⚠️ Potential issue | 🟠 Major

Fix the Commons URL query field format from underscore to hyphen.

The URL at line 224 constructs ${resource.type}_id:${resource.id} (e.g., consortium_id:123), but DataCite Commons expects the field name with a hyphen: ${resource.type}-id:${resource.id} (e.g., consortium-id:123). Update the query parameter to use hyphens to match the DataCite API field naming convention that Commons recognizes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ActionButtons.tsx` around lines 209 - 234, In ViewInCommons,
the Commons query uses an underscore field name
(`${resource.type}_id:${resource.id}`) but DataCite Commons expects a hyphenated
field (e.g., `consortium-id:123`); update the doi.org branch that builds href so
the query uses `${resource.type}-id:${resource.id}` (adjust the string
interpolation where href is constructed) while keeping the rest of the
URL/params intact.
🟡 Minor comments (12)
src/components/ui/combobox.tsx-281-293 (1)

281-293: ⚠️ Potential issue | 🟡 Minor

children destructured but never rendered in ComboboxChipsInput.

children is extracted from props on Line 283 but is not used in the returned JSX (Lines 287-291). This silently drops any children passed by consumers. If children are intentionally unsupported, remove the destructuring so they flow through ...props; if they should be rendered, add them to the JSX.

Fix: remove unused destructuring
 function ComboboxChipsInput({
   className,
-  children,
   ...props
 }: ComboboxPrimitive.Input.Props) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/combobox.tsx` around lines 281 - 293, The
ComboboxChipsInput function destructures children but never uses them, which
drops any passed children; fix by removing children from the parameter
destructuring so children remain in ...props and flow through to
<ComboboxPrimitive.Input>, i.e. change the function signature to accept ({
className, ...props }: ComboboxPrimitive.Input.Props) and leave the returned JSX
for ComboboxPrimitive.Input unchanged; alternatively, if children should be
rendered, include {children} inside the returned JSX—refer to the
ComboboxChipsInput function and the <ComboboxPrimitive.Input> call to implement
the chosen fix.
src/app/globals.css-39-44 (1)

39-44: ⚠️ Potential issue | 🟡 Minor

Remove unused --color-primary-light-blue and --color-primary-dark-blue tokens.

Lines 39-40 define --color-primary-light-blue (#00b1e2) and --color-primary-dark-blue (#243b54), but these tokens are not referenced anywhere in the codebase. The active replacements—--color-datacite-blue-light and --color-datacite-blue-dark on lines 41-42—are used in src/constants.ts and src/components/Badges.tsx. Remove the unused primary-* tokens to eliminate dead code.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/globals.css` around lines 39 - 44, Remove the two unused CSS custom
properties --color-primary-light-blue and --color-primary-dark-blue from
src/app/globals.css; they are dead code and should be deleted so only the active
tokens --color-datacite-blue-light and --color-datacite-blue-dark remain (these
are referenced by src/constants.ts and src/components/Badges.tsx). Verify no
other files reference --color-primary-light-blue or --color-primary-dark-blue
and run a quick repo-wide search to confirm before committing.
src/app/[[...id]]/Header.tsx-5-5 (1)

5-5: ⚠️ Potential issue | 🟡 Minor

useId shadows React 19's built-in useId.

React 19 exports its own useId hook (for stable accessibility IDs). The local useId from @/hooks extracts URL params — a completely different contract. Developers relying on auto-imports or muscle memory could silently import the wrong hook. Consider renaming to something unambiguous, e.g. useResourceId or useSlugId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[[...id]]/Header.tsx at line 5, The local hook useId from "@/hooks"
conflicts with React 19's built-in useId; rename the local hook import to an
unambiguous name (e.g., useResourceId or useSlugId) and update all usages in
Header.tsx (and any other files) to that new identifier to avoid accidental
auto-import collisions with React's useId; optionally, update the original
export in the source hooks module to export the hook under the new name (or add
a named alias) so callers can import the clarified name consistently.
src/app/layout.tsx-29-31 (1)

29-31: ⚠️ Potential issue | 🟡 Minor

Hardcoded Usersnap project key — consider using an environment variable.

The key b4393e90-ec13-4338-b299-7b6f122b7de3 is embedded directly in source. While it's a public client-side key, externalising it to an env var (e.g. NEXT_PUBLIC_USERSNAP_KEY) makes it easier to swap keys across environments (staging vs. production) without a code change and avoids leaking the production key into branches/forks.

♻️ Proposed refactor
-        <Script id="feedback-button">
-          {`(function(){window.onUsersnapCXLoad=function(e){e.init()};var e=document.createElement("script");e.defer=1,e.src="https://widget.usersnap.com/global/load/b4393e90-ec13-4338-b299-7b6f122b7de3?onload=onUsersnapCXLoad",document.getElementsByTagName("head")[0].appendChild(e)})();`}
-        </Script>
+        {process.env.NEXT_PUBLIC_USERSNAP_KEY && (
+          <Script id="feedback-button">
+            {`(function(){window.onUsersnapCXLoad=function(e){e.init()};var e=document.createElement("script");e.defer=1,e.src="https://widget.usersnap.com/global/load/${process.env.NEXT_PUBLIC_USERSNAP_KEY}?onload=onUsersnapCXLoad",document.getElementsByTagName("head")[0].appendChild(e)})();`}
+          </Script>
+        )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/layout.tsx` around lines 29 - 31, The inline Usersnap script in the
Script element (id="feedback-button") hardcodes the project key; update it to
read from an environment variable (e.g. NEXT_PUBLIC_USERSNAP_KEY via
process.env.NEXT_PUBLIC_USERSNAP_KEY) and interpolate that value into the script
string used in layout.tsx instead of the literal GUID, adding a simple guard or
fallback behavior if the env var is missing so the page doesn't break; locate
the Script with id="feedback-button" and replace the hardcoded key with the env
variable reference.
src/components/ui/card.tsx-45-50 (1)

45-50: ⚠️ Potential issue | 🟡 Minor

Replace text-md with text-base — it's not a standard or custom Tailwind utility.

Tailwind's default font-size scale goes text-xs, text-sm, text-base, text-lg, etc., with no text-md. The globals.css file uses Tailwind CSS v4's @theme syntax and defines custom colors, spacing, and shadows but no font-size tokens. Since text-md generates no CSS, text-sm on the preceding line silently wins, which is likely unintended.

Fix
       className={cn(
         "text-muted-foreground text-sm",
-        "font-normal text-md",
+        "font-normal text-base",
         barlow.className,
         className,
       )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/card.tsx` around lines 45 - 50, In the Card component's
className composition (the cn(...) call) replace the non-existent Tailwind
utility "text-md" with the correct "text-base" so the intended font-size wins
over "text-sm"; update the string literal inside the cn call (in
src/components/ui/card.tsx where className is composed) to use "text-base"
alongside "text-muted-foreground text-sm", Barlow's class, and the passed-in
className.
src/components/Badges.tsx-4-5 (1)

4-5: ⚠️ Potential issue | 🟡 Minor

Invalid Tailwind classes: p-y-0 and p-x-1 should be py-0 and px-1.

Tailwind CSS uses py-0 / px-1 for vertical/horizontal padding, not p-y-0 / p-x-1. These classes will be silently ignored, resulting in no padding being applied.

🐛 Proposed fix
 const DEFAULT_CLASS =
-  "rounded-[40px] text-[0.8em] text-datacite-blue-dark bg-datacite-blue-light/20 p-y-0 p-x-1 border-none";
+  "rounded-[40px] text-[0.8em] text-datacite-blue-dark bg-datacite-blue-light/20 py-0 px-1 border-none";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Badges.tsx` around lines 4 - 5, The DEFAULT_CLASS string in
Badges.tsx uses invalid Tailwind tokens `p-y-0` and `p-x-1`; update the class
constant DEFAULT_CLASS to replace those with the correct Tailwind utilities
`py-0` and `px-1` so vertical and horizontal padding are applied (keep the rest
of the class string intact).
src/app/[[...id]]/page.tsx-15-15 (1)

15-15: ⚠️ Potential issue | 🟡 Minor

Remove debug console.log.

This logs the full search params object on every page load and appears to be a leftover debug statement.

Proposed fix
-  console.log(await searchParams);
-
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[[...id]]/page.tsx at line 15, Remove the debug console.log
statement that prints the search params in the page component: delete the line
containing "console.log(await searchParams)" from src/app/[[...id]]/page.tsx (or
replace with appropriate analytics/logging if intentional), ensuring no leftover
debug prints remain in the page rendering logic.
src/components/Breadcrumbs.tsx-38-38 (1)

38-38: ⚠️ Potential issue | 🟡 Minor

Remove unused BreadcrumbData type export.

BreadcrumbData is not imported or referenced anywhere in the codebase. Since Breadcrumbs no longer accepts a pages prop, this exported type is dead code and should be removed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Breadcrumbs.tsx` at line 38, Remove the unused exported type
BreadcrumbData from the Breadcrumbs component file: locate the exported type
declaration "export type BreadcrumbData = { title: string; href?: string }" and
delete it (since Breadcrumbs no longer accepts a pages prop and the type is not
referenced anywhere), then run a quick TypeScript build or search to confirm no
remaining references to BreadcrumbData exist.
src/app/[[...id]]/page.tsx-12-12 (1)

12-12: ⚠️ Potential issue | 🟡 Minor

Throw an Error object instead of a string literal.

throw "Incorrect ID format" throws a raw string. Error boundaries and logging tools expect Error instances (with stack traces). Use throw new Error("Incorrect ID format") or call notFound() if a malformed slug should yield a 404.

Proposed fix
-  if (slug && slug.length > 1) throw "Incorrect ID format";
+  if (slug && slug.length > 1) notFound();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[[...id]]/page.tsx at line 12, The current slug validation in
page.tsx uses a string literal in the throw (if (slug && slug.length > 1) throw
"Incorrect ID format";), which should be replaced with an Error object for
proper stack traces and tooling; update that check to either throw new
Error("Incorrect ID format") or, if a malformed slug should return a 404, call
Next.js's notFound() instead, keeping the same condition that inspects slug and
slug.length.
src/components/cards/Cards.tsx-554-563 (1)

554-563: ⚠️ Potential issue | 🟡 Minor

Typo: "Fomat documentation" → "Format documentation".

Line 559: text="Fomat documentation" is missing the 'r'.

Proposed fix
     <LearnMore
-      text="Fomat documentation"
+      text="Format documentation"
       href="https://datacite-metadata-schema.readthedocs.io/en/4/properties/format/"
     />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/cards/Cards.tsx` around lines 554 - 563, The string in the
FormatsDescription component has a typo: update the LearnMore component's text
prop from "Fomat documentation" to "Format documentation" inside the
FormatsDescription JSX (referencing the FormatsDescription constant and the
LearnMore component/text prop) so the displayed label is correct.
src/components/cards/Cards.tsx-191-200 (1)

191-200: ⚠️ Potential issue | 🟡 Minor

Copy-paste error: Publisher description says "funding sources" instead of "publishers".

Line 193 reads "The Publisher property connects resources to funding sources" — this is copied from the FundingReferences description. It should describe the Publisher property.

Proposed fix
 const PublisherDescription = (
   <>
-    The Publisher property connects resources to funding sources. Add
+    The Publisher property identifies the entity responsible for making the resource available. Add
     publisherIdentifiers with ROR IDs for the highest impact.{" "}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/cards/Cards.tsx` around lines 191 - 200, The
PublisherDescription JSX constant contains incorrect copy ("connects resources
to funding sources"); update the text in the PublisherDescription constant so it
correctly refers to publishers (e.g., "The Publisher property connects resources
to publishers. Add publisherIdentifiers with ROR IDs for the highest impact.")
and keep the existing LearnMore link and structure intact; edit the
PublisherDescription definition to replace the erroneous "funding sources"
wording with "publishers".
src/data/fetch.ts-58-58 (1)

58-58: ⚠️ Potential issue | 🟡 Minor

Remove console.info(resource) — debug logging left in production code.

This logs the entire resource object on every fetch, which leaks internal data structures to the browser console and adds noise.

Proposed fix
-  console.info(resource);
   return resource;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/data/fetch.ts` at line 58, Remove the stray debug logging call
console.info(resource); — delete that statement or replace it with a controlled
debug log (e.g., use a logger.debug or wrap in a NODE_ENV check) so the full
resource object is not printed in production; locate the console.info(resource)
invocation and either remove it or guard it with a conditional
(process.env.NODE_ENV !== 'production') or use the project's logger at debug
level.
🧹 Nitpick comments (27)
src/components/ui/item.tsx (4)

43-43: Trailing space in default size value

"p-4 gap-4 " has a stray trailing space. cn/clsx will handle it silently, but it's worth cleaning up.

🧹 Proposed fix
-        default: "p-4 gap-4 ",
+        default: "p-4 gap-4",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/item.tsx` at line 43, The default size string in
src/components/ui/item.tsx contains a trailing space ("p-4 gap-4 "); remove the
stray space so the value becomes "p-4 gap-4". Locate the size/default mapping
(the `default` key in the size variants or `itemVariants`/size object) and
update the string, then run linters/formatters to ensure no leftover whitespace
remains.

156-180: ItemHeader and ItemFooter share identical class strings

Both apply "flex basis-full items-center justify-between gap-2" — only data-slot differs. If the visual treatment ever diverges they'll need to be updated in two places. A tiny shared constant makes the intentional parity explicit.

♻️ Suggested refactor
+const itemEdgeClasses = "flex basis-full items-center justify-between gap-2"
+
 function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
   return (
     <div
       data-slot="item-header"
-      className={cn(
-        "flex basis-full items-center justify-between gap-2",
-        className
-      )}
+      className={cn(itemEdgeClasses, className)}
       {...props}
     />
   )
 }

 function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
   return (
     <div
       data-slot="item-footer"
-      className={cn(
-        "flex basis-full items-center justify-between gap-2",
-        className
-      )}
+      className={cn(itemEdgeClasses, className)}
       {...props}
     />
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/item.tsx` around lines 156 - 180, ItemHeader and ItemFooter
duplicate the same class string; extract the shared class list into a single
constant (e.g., ITEM_CONTAINER_CLASSES) and use that constant in both ItemHeader
and ItemFooter to avoid duplication and make future style changes in one place;
update the JSX in both components to reference the new constant while keeping
the data-slot and props behavior unchanged.

43-43: Stray trailing space in the default size value

"p-4 gap-4 " has a trailing space that cn silently tolerates but is worth cleaning up.

🧹 Proposed fix
-        default: "p-4 gap-4 ",
+        default: "p-4 gap-4",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/item.tsx` at line 43, The default size value in the sizes
object (the `default` key used by the Item component in
src/components/ui/item.tsx) contains a stray trailing space ("p-4 gap-4 ");
remove the trailing space so the value is "p-4 gap-4" to keep class strings tidy
and avoid silent inconsistencies when passed into `cn` or similar utilities.

156-180: ItemHeader and ItemFooter share identical styling — consider a shared base

Both components produce "flex basis-full items-center justify-between gap-2" with only the data-slot attribute differing. If the visual language for headers and footers ever diverges, it's easy to forget to update both. A single shared itemEdgeVariants string (or inline constant) would make the relationship explicit.

♻️ Suggested refactor
+const itemEdgeClasses =
+  "flex basis-full items-center justify-between gap-2"

 function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
   return (
     <div
       data-slot="item-header"
-      className={cn(
-        "flex basis-full items-center justify-between gap-2",
-        className
-      )}
+      className={cn(itemEdgeClasses, className)}
       {...props}
     />
   )
 }

 function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {
   return (
     <div
       data-slot="item-footer"
-      className={cn(
-        "flex basis-full items-center justify-between gap-2",
-        className
-      )}
+      className={cn(itemEdgeClasses, className)}
       {...props}
     />
   )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/item.tsx` around lines 156 - 180, Extract the shared class
string into a single constant (e.g., itemEdgeVariants) and use it in both
ItemHeader and ItemFooter to avoid duplication; update ItemHeader and ItemFooter
to call cn(itemEdgeVariants, className) and keep their distinct data-slot
attributes unchanged so only the class list is centralized for future updates.
src/app/error.tsx (1)

3-12: Missing reset prop leaves users with no recovery path

Next.js error.tsx receives a reset: () => void callback alongside error. Without it, a user hitting this boundary can only hard-reload the page. Adding a "Try again" button is the conventional pattern.

♻️ Proposed fix
-export default function ErrorPage({
-  error,
-}: {
-  error: Error & { digest?: string };
-}) {
+export default function ErrorPage({
+  error,
+  reset,
+}: {
+  error: Error & { digest?: string };
+  reset: () => void;
+}) {
   return (
     <div>
       <h2>{error.message}</h2>
+      <button onClick={reset}>Try again</button>
     </div>
   );
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/error.tsx` around lines 3 - 12, The ErrorPage component currently
only accepts { error } and renders the message, but Next.js passes a reset: ()
=> void which should be used to let users retry; update the ErrorPage signature
to accept the second prop reset (i.e., function ErrorPage({ error, reset }: {
error: Error & { digest?: string }; reset: () => void })) and add a "Try again"
button that calls reset on click, ensuring the button is rendered alongside the
error message in the component returned by ErrorPage.
src/components/ui/input-group.tsx (2)

71-76: Click-to-focus: querySelector("input") could match a nested input unexpectedly.

e.currentTarget.parentElement?.querySelector("input") finds the first <input> inside the parent, which works when there is a single InputGroupInput. If someone ever nests multiple inputs (e.g., an inline search input inside a chip addon), this would focus the wrong element. Using querySelector('[data-slot="input-group-control"]') would be more precise and self-documenting.

Suggested tweak
-        e.currentTarget.parentElement?.querySelector("input")?.focus()
+        e.currentTarget.parentElement
+          ?.querySelector<HTMLElement>('[data-slot="input-group-control"]')
+          ?.focus()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/input-group.tsx` around lines 71 - 76, In the onClick
handler inside the InputGroup component replace the broad
e.currentTarget.parentElement?.querySelector("input") lookup with a precise
selector that targets the input slot (e.g.,
e.currentTarget.parentElement?.querySelector('[data-slot="input-group-control"]'))
so clicks focus the intended InputGroupInput rather than any nested input;
update the selector in the onClick callback and ensure the InputGroupInput
renders the matching data-slot attribute.

100-117: Button receives no size prop — relies on class merging to override its default.

InputGroupButton omits size from Button's props and only passes sizing via className. Button will apply its own default size classes, then cn(inputGroupButtonVariants({ size }), className) overrides them. This works because cn uses twMerge, but it's subtle. A brief comment explaining the intentional override would aid future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/input-group.tsx` around lines 100 - 117, The
InputGroupButton intentionally omits the Button "size" prop and passes sizing
only via className so that inputGroupButtonVariants({ size }) merged by
cn/twMerge overrides Button's default size classes; add a concise inline comment
above the InputGroupButton function (or above the Button JSX) stating that
omission is intentional, referencing InputGroupButton, inputGroupButtonVariants,
Button, and cn (twMerge) to clarify that class merging is relied upon to
override Button's default size behavior for future maintainers.
src/hooks.ts (2)

9-12: useId name shadows React's built-in useId hook.

React 18+ exports a useId hook. While this custom hook is in a different module and won't technically conflict at runtime, the name collision can cause confusion during imports and code review. Consider renaming to something more descriptive like useRouteId or useSlugId.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks.ts` around lines 9 - 12, The custom hook useId shadows React's
built-in useId; rename the function (and its exports/imports) to a clearer name
such as useRouteId or useSlugId to avoid confusion—update the function
declaration export function useId() to export function useRouteId() (or chosen
name), adjust internal references (the useParams<{ id?: string[] }>()
destructuring and slug handling remain the same), and update all imports where
useId is consumed to the new name so callers reference useRouteId (or the chosen
identifier) instead of useId.

48-61: Stale closure risk: resource! is captured outside the query key.

resource is used inside queryFn but is not part of queryKey ([id, filters, key]). TanStack Query determines when to refetch based on the query key, so if resource were to change shape while id stays the same, the cached result would not invalidate. In practice this is likely safe since resource is derived from id, but the non-null assertion on Line 57 is fragile — if the enabled guard is ever changed, resource! silently becomes a runtime error.

Consider a safer pattern:

Proposed improvement
 export function useQueryResource<R>(
   key: string,
   fetch: (resource: Resource, filters: Filters) => Promise<R>,
   initialData?: R,
 ) {
   const { data: resource } = useResource();
 
   return useQueryId<R>(
     key,
-    (_, filters) => fetch(resource!, filters),
+    (_, filters) => {
+      if (!resource) throw new Error("resource unexpectedly null");
+      return fetch(resource, filters);
+    },
     initialData,
     !!resource,
   );
 }

This narrows the type without a non-null assertion, making any future enabled-guard regressions surface as an explicit error rather than undefined behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks.ts` around lines 48 - 61, The useQueryResource function captures
resource with a non-null assertion (resource!) inside the queryFn which risks
stale-closure or runtime errors; update useQueryResource (and its call to
useQueryId) to avoid the non-null assertion by either including resource in the
queryKey or narrowing resource before creating the query: call useResource(), if
resource is falsy return useQueryId with enabled=false (or throw) else create
the queryFn that closes over the already-narrowed resource (no !), keep enabled
set to !!resource, and ensure the queryKey passed to useQueryId includes
resource (or a stable identifier derived from it) so TanStack Query revalidates
when resource shape changes; adjust symbols: useQueryResource, useResource,
useQueryId, queryFn, and queryKey accordingly.
src/app/globals.css (1)

55-89: Missing --card-title in .dark theme.

--card-title is defined in :root (Line 61) and mapped to --color-card-title in the theme (Line 37), but the .dark block (Lines 91–123) doesn't override it. If dark mode should use a different card-title color, add an override; if the light-mode value is intentional for both themes, a brief comment would clarify that.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/globals.css` around lines 55 - 89, The .dark theme is missing an
override for the --card-title variable: add a --card-title declaration inside
the .dark block (next to other overrides like --card, --card-foreground) to set
the dark-mode card title color, or if the :root value is intentional for both
themes, add a short comment inside the .dark block referencing --card-title and
--color-card-title to indicate the shared value so reviewers know it was
deliberate.
src/components/ui/combobox.tsx (2)

98-133: Extremely long className string reduces readability.

The className on Lines 124-125 is a single line with many Tailwind utilities and data-attribute selectors. Consider extracting subsets into named cva variants or breaking the string across multiple lines (using template literals or an array joined by spaces) for maintainability.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/combobox.tsx` around lines 98 - 133, The ComboboxContent
component has an unreadably long className string; refactor it by extracting the
bulky Tailwind/data-attribute rules into a named constant (e.g.,
comboboxContentClass) using a class-variance-authority (cva) definition or into
an array/template literal joined by spaces, then replace the inline string in
the ComboboxPrimitive.Popup className with that constant combined with the
incoming className; target the ComboboxContent function and the
ComboboxPrimitive.Popup call, and move grouped rules (animations, positioning,
data-[slot] modifiers, and chips/min-width logic) into cva variants or separate
variables so the JSX line becomes concise and maintainable.

233-248: Remove redundant type intersection in ComboboxChips props.

React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> & ComboboxPrimitive.Chips.Props is redundant. ComboboxPrimitive.Chips.Props already extends BaseUIComponentProps<'div'>, which includes all React div component props with ref support. Additionally, ComboboxChips is not a forwardRef wrapper, so the ref is not actually forwarded. Use ComboboxPrimitive.Chips.Props alone to match the pattern used consistently throughout the file (e.g., ComboboxSeparator, ComboboxTrigger, ComboboxClear).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ui/combobox.tsx` around lines 233 - 248, The ComboboxChips
component signature should stop using the redundant intersection of
React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
ComboboxPrimitive.Chips.Props; update the props type to just
ComboboxPrimitive.Chips.Props (since it already includes div props and ref
support) and ensure the component remains a plain function (not forwardRef) to
match other components like ComboboxSeparator/ComboboxTrigger/ComboboxClear;
remove the unnecessary ComponentPropsWithRef usage and any now-unused imports if
present.
src/components/RadialChart.tsx (1)

45-48: Remove the commented-out ChartTooltip block.

This dead code has been commented out since at least the initial implementation and is unlikely to be re-enabled. If tooltip support is genuinely planned, a follow-up issue is a better tracking mechanism.

♻️ Proposed cleanup
-        {/* <ChartTooltip
-          cursor={false}
-          content={<ChartTooltipContent hideLabel />}
-        /> */}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/RadialChart.tsx` around lines 45 - 48, Remove the
dead/commented-out tooltip JSX from RadialChart.tsx: delete the commented
ChartTooltip block that references ChartTooltip and ChartTooltipContent (the
three commented lines) so the file no longer contains leftover commented code;
if tooltip work is planned, create a separate issue instead of keeping commented
markup.
src/app/Header.tsx (1)

11-11: Remove the empty className="" from Image.

The prop has no effect and adds noise.

♻️ Proposed cleanup
-          <Image src={logo} alt="DataCite logo" height={75} className="" />
+          <Image src={logo} alt="DataCite logo" height={75} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/Header.tsx` at line 11, The Image element in Header.tsx includes an
empty prop className="" which is no-op and should be removed; edit the JSX
containing <Image src={logo} alt="DataCite logo" height={75} className="" /> to
drop the className attribute (or only pass className when a non-empty value is
needed), keeping the Image import/usage and other props unchanged.
src/app/[[...id]]/Header.tsx (1)

12-13: Redundant text-4xl and inconsistent className composition.

H2 from Headings.tsx already applies text-4xl as part of its base classes via cn(). Repeating it here is a no-op. Additionally, the template literal diverges from the cn() pattern used throughout the rest of the codebase for conditional class composition.

♻️ Proposed cleanup
-    <H2 className={`text-4xl w-full ${resource ? "" : "opacity-70"}`}>
+    <H2 className={cn("w-full", !resource && "opacity-70")}>

Add the cn import:

+"use client";
+
+import { cn } from "@/lib/utils";
 import { H2 } from "@/components/datacite/Headings";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[[...id]]/Header.tsx around lines 12 - 13, Remove the redundant
"text-4xl" from the H2 usage and switch to using the shared cn() helper for
class composition: import cn and rewrite the H2 className to call cn("w-full",
resource ? "" : "opacity-70") (or equivalent with conditional entry) so the base
classes from Headings.tsx are not duplicated and the conditional class follows
the cn() pattern; reference the H2 component and the resource/id props when
making this change.
src/components/cards/OverviewCard.tsx (1)

14-15: Error display may render [object Object] instead of a readable message.

error from React Query is typically an Error instance. The template literal `Error: ${error}` will call .toString() on it, which may produce Error: Error: actual message (doubled prefix). Consider using error.message for a cleaner display, or rendering a proper error UI component.

♻️ Suggested fix
-  if (isError) return `Error: ${error}`;
+  if (isError) return `Error: ${error instanceof Error ? error.message : error}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/cards/OverviewCard.tsx` around lines 14 - 15, The current
error branch in OverviewCard returns a template string which can render
unhelpful "[object Object]" or duplicate "Error:" prefixes; update the isError
branch in the OverviewCard component to return a proper React node that displays
the underlying error message (use error?.message or extract message from the
React Query error object) or render a small Error UI component instead of
`\`Error: ${error}\`` so the message is readable and safe.
src/components/Badges.tsx (1)

19-21: Default show = true is dead code since show is a required prop.

The type { show: boolean } makes show required, so the default value = true on Line 20 never triggers. Either make the prop optional (show?: boolean) or remove the default.

♻️ Suggested fix — make `show` optional
-export function HighImpactBadge(props: { show: boolean } & BadgeProps) {
+export function HighImpactBadge(props: { show?: boolean } & BadgeProps) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Badges.tsx` around lines 19 - 21, The default assignment "show
= true" in the HighImpactBadge component is dead because the prop is typed as
required; change the prop type from "{ show: boolean } & BadgeProps" to "{
show?: boolean } & BadgeProps" so the default will take effect (leave the
destructuring "const { show = true, className, ...badgeProps } = props" as is);
alternatively, if you prefer to keep show required, remove the "= true" default
in the destructuring—update either the HighImpactBadge prop type or its
destructuring accordingly.
src/components/ResourceTypesChart.tsx (1)

62-83: Legend truncation silently hides entries with no visual cue.

When limit={5} is applied (Line 69), any resource types beyond the first 5 are silently hidden from the legend. Consider adding a summary item like "+ N more" so users know additional types exist in the chart but aren't listed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ResourceTypesChart.tsx` around lines 62 - 83,
ChartLegendContent currently slices props.payload and hides entries without any
indicator; compute the filtered list (filtering out item.type === "none") into a
variable, apply the slice using props.limit, and if filtered.length >
(props.limit ?? filtered.length) append a final legend entry that displays a "+
N more" summary (use a unique key like `more-${N}` and match the existing
styling, e.g., a small square followed by the text) so users see how many
entries were omitted; ensure you don't render the summary when hidden count is
<= 0.
src/components/DistributionChart.tsx (1)

8-11: Props type accepts all div attributes but only className is forwarded.

Props extends React.HTMLAttributes<HTMLDivElement>, implying callers can pass style, onClick, etc. However, only className is read from props (Line 24) — other attributes are silently discarded. Consider either spreading the rest onto the outer <div> or narrowing the type to only accept className.

♻️ Option: spread remaining props
 export default function DistributionChart(props: Props) {
-  const { property, data } = props;
+  const { property, data, className, ...divProps } = props;

   const [displayAll, setDisplayAll] = useState(false);
   const toggleDisplayAll = () => setDisplayAll(!displayAll);

   if (data.length === 0) return null;

   const displayedData = displayAll ? data : data.slice(0, 3);

   return (
-    <div className={cn("w-full flex flex-col h-min gap-2", props.className)}>
+    <div className={cn("w-full flex flex-col h-min gap-2", className)} {...divProps}>

Also applies to: 23-24

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DistributionChart.tsx` around lines 8 - 11, The Props
interface currently extends React.HTMLAttributes<HTMLDivElement> but the
DistributionChart component only reads className and drops other attributes; fix
by changing the component signature to destructure className and the rest (e.g.,
({ property, data, className, ...rest }: Props)) and spread ...rest onto the
outer <div> (the container rendered by DistributionChart) so that style,
onClick, aria-*, etc. are forwarded; alternatively, if you want to restrict
accepted props, narrow Props to only include className instead of extending
HTMLAttributes.
src/components/datacite/Headings.tsx (1)

4-6: H1, H5, H6 have no base styles or dmSans font — intentional placeholders?

H2–H4 apply typography classes and the dmSans font, but H1, H5, and H6 pass through with cn("", className), making them bare HTML elements. If these are intentional stubs for future use, a brief comment would clarify intent; otherwise they should get consistent base styling.

Also applies to: 39-45

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/datacite/Headings.tsx` around lines 4 - 6, H1, H5, and H6 in
Headings.tsx are currently passthroughs (cn("", className)) and lack the base
typography and dmSans font used by H2–H4; either add the same base classes plus
the dmSans font to these components (mirror the pattern used in H2/H3/H4) —
e.g., include the shared base class tokens and the dmSans class in the cn call
for H1, H5, and H6 — or, if they are intentionally left unstyled, add a brief
comment above the H1/H5/H6 functions stating they are deliberate placeholders so
reviewers know it’s by design.
src/data/fetch.ts (4)

125-132: Formatters use positional array indices — brittle coupling to field order in COMPLETENESS_FIELDS.

All formatters (e.g., formatCreators line 126: p[0], p.slice(1, 5), p[5]) depend on the exact ordering of fields in the corresponding COMPLETENESS_FIELDS entry. If someone reorders a field in CREATOR_FIELDS.present, the formatter silently produces wrong mappings. This is a maintenance risk worth documenting at minimum.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/data/fetch.ts` around lines 125 - 132, formatCreators relies on
positional indices into the parsed array (p[0], p.slice(1,5), p[5]) which
tightly couples it to the ordering of COMPLETENESS_FIELDS/CREATOR_FIELDS.present
and will break if field order changes; change createFormat/formatCreators to map
by field name instead of fixed indices: update createFormat to accept a schema
(e.g., the CREATOR_FIELDS.present array) or a name->index map and use indexOf or
a precomputed lookup to reference fields by name (e.g., "creators",
"affiliation", "nameIdentifier") when constructing the returned object, and
update formatCreators to use those named lookups rather than hardcoded numeric
indices.

62-63: getData has no error handling — non-200 responses or network errors will throw cryptic errors.

If fetchDatacite returns a non-200 response, .json() may still parse (e.g., an error body), and the cast to T will silently produce a misshapen object. If the response isn't JSON at all, .json() throws a generic parse error. Consider adding a response status check.

Proposed fix
-const getData = async <T extends { data: unknown }>(url: string) =>
-  ((await (await fetchDatacite(url)).json()) as T).data as T["data"];
+const getData = async <T extends { data: unknown }>(url: string) => {
+  const res = await fetchDatacite(url);
+  if (!res.ok) throw new Error(`API request failed: ${res.status} ${res.statusText}`);
+  return ((await res.json()) as T).data as T["data"];
+};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/data/fetch.ts` around lines 62 - 63, getData currently assumes
fetchDatacite succeeds and returns valid JSON; add robust error handling by
awaiting the response object from fetchDatacite, checking response.ok (and if
not, read text/json body for details) and throwing a clear, typed error that
includes status and body; wrap the .json() call in try/catch to catch parse
errors and rethrow a descriptive error mentioning the URL and response status;
update the getData<T> function to perform these checks before casting to
T["data"] so you never silently return a malformed object.

37-51: Children fetch assumes getData always returns an array for ApiResource<true> — a 404 or error response could crash .map().

If the children endpoint returns a non-array (e.g., the provider has no associated clients endpoint or the API returns an error), .map() on line 43 will throw a runtime error.

Proposed fix
     children:
       data.type !== "clients"
-        ? (
+        ? ((
             await getData<ApiResource<true>>(
               `${data.attributes.memberType === "consortium" ? "providers" : "clients"}?page[size]=1000&${data.attributes.memberType === "consortium" ? "consortium" : "provider"}-id=${data.id}`,
             )
-          ).map((c) => ({
+          ) || []).map((c) => ({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/data/fetch.ts` around lines 37 - 51, The children mapping assumes
getData<ApiResource<true>>(...) always returns an array; to prevent runtime
crashes when the API returns 404/non-array or an error, await getData(...) into
a local (e.g., childrenData), check Array.isArray(childrenData) (or coerce with
childrenData ?? []), and only call .map(...) when it's an array; otherwise
return an empty array and optionally log or surface the error. Update the
expression around getData<ApiResource<true>>(...) used in the children property
so it safely handles non-array responses (preserve the existing mapping of c.id,
c.attributes.name, and subtype logic that uses data.attributes.memberType and
c.type).

14-60: Recursive fetchResource has no depth guard — a malformed API response could cause infinite recursion.

fetchResource calls itself recursively (line 52) to resolve the parent chain. The DataCite hierarchy is shallow (≤3 levels), but there's no protection against cycles or unexpectedly deep chains. A simple depth counter would safeguard against this.

Proposed fix sketch
 export async function fetchResource(
   id: string | null,
+  depth = 0,
 ): Promise<Resource | null> {
   if (!id) return null;
+  if (depth > 5) return null;
   // ...
-    parent: await fetchResource(
+    parent: await fetchResource(
       data.type === "clients"
         ? data.relationships.provider.data.id
         : data.relationships.consortium?.data.id || null,
+      depth + 1,
     ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/data/fetch.ts` around lines 14 - 60, The recursive fetchResource call can
loop indefinitely on malformed API data; add a depth guard to fetchResource
(e.g., add an optional parameter like depth: number = 0 and a MAX_DEPTH
constant) and immediately return null (or stop resolving parent) when depth >=
MAX_DEPTH; increment depth when calling fetchResource for the parent (and also
thread depth through any internal helper calls like the parent resolution
branch) so cycles or unexpectedly deep chains are cut off. Optionally, you can
also add a simple visited-ids set passed through recursion to detect cycles by
id (use the id from ApiResource) and bail out if an id repeats.
src/components/PresentBar.tsx (1)

53-64: Consider removing redundant tickFormatter and tick props on the hidden YAxis.

Since hide is set (line 63), the tickFormatter, tick, interval, and orientation props on YAxis have no visible effect. If includeHidden is only needed for layout spacing, you can simplify by removing the formatting props.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/PresentBar.tsx` around lines 53 - 64, In PresentBar's YAxis
component the props tickFormatter, tick, interval and orientation are redundant
because hide is true (and includeHidden is used only for spacing); remove
tickFormatter, tick, interval and orientation from the <YAxis> element (leave
dataKey, type, axisLine, tickLine, includeHidden and hide) so the hidden axis
remains for layout without unnecessary formatting props.
src/util.ts (1)

127-139: toPresentProps / toDistributionProps will throw if the API omits a requested field.

findInPresent / findInDistribution return undefined when the completeness API doesn't include a requested field in its response (no defaultValue is passed to findBuilder). This propagates to toPresentProps(undefined) which throws "Present item is undefined". While fail-fast is reasonable, a single missing field will crash the entire card.

Consider providing a fallback so missing fields degrade to 0% instead of throwing:

Proposed fix
  const findInPresent = findBuilder(
    json.present,
    (item, desired: string) => item.field === desired,
+   { field: "", percent: 0, count: 0, absent_count: 0 } as Present,
  );
  const findInDistribution = findBuilder(
    json.distribution,
    (item, desired: string) => item.field === desired,
+   { field: "", values: [] } as Distribution,
  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util.ts` around lines 127 - 139, findInPresent/findInDistribution can
return undefined and passing that into toPresentProps/toDistributionProps
throws; ensure missing fields degrade to 0% by supplying a safe fallback before
mapping. Change the pipeline that builds present and distribution so that after
mapping with findInPresent/findInDistribution you coalesce undefined to a
default object (e.g. { field: desiredField, completeness: 0 } or equivalent
shape) or supply a defaultValue into findBuilder; then pass the safe object into
toPresentProps/toDistributionProps so a missing API field becomes a 0% entry
rather than throwing.
src/constants.ts (1)

17-59: Other key in PALETTE_OTHER is silently overridden by PALETTE_RESOURCE_TYPE.

PALETTE_OTHER.Other is "gray" (line 19), but it's overridden by PALETTE_RESOURCE_TYPE.Other = "#C59088" (line 58). If the "gray" value in PALETTE_OTHER is intentionally a generic fallback for non-resource-type contexts, this is fine. But if any consumer expects PALETTE_RESOURCE_TYPE to include the original "gray" for Other, it won't. Just flagging for awareness since the override is easy to miss.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/constants.ts` around lines 17 - 59, PALETTE_OTHER.Other is being shadowed
by PALETTE_RESOURCE_TYPE.Other when you spread PALETTE_OTHER into
PALETTE_RESOURCE_TYPE; decide which value should win and fix accordingly: either
remove or rename the duplicate key in PALETTE_RESOURCE_TYPE (e.g., delete the
"Other" entry in PALETTE_RESOURCE_TYPE) or rename PALETTE_OTHER.Other to a
distinct fallback key (e.g., Fallback or Generic) and update all consumers, or
change the spread order to have PALETTE_OTHER spread last so PALETTE_OTHER.Other
overrides; reference PALETTE_OTHER and PALETTE_RESOURCE_TYPE to locate the
change.

Comment on lines +23 to +24
if (Array.isArray(value))
for (const v in value) urlSearchParams.append(key, v);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: for...in iterates array indices, not values.

for (const v in value) yields the string indices "0", "1", … — not the actual array elements. This corrupts multi-valued query parameters during the lowercase redirect (e.g., ?foo=a&foo=b becomes ?foo=0&foo=1).

🐛 Fix: use `for...of`
       if (Array.isArray(value))
-        for (const v in value) urlSearchParams.append(key, v);
+        for (const v of value) urlSearchParams.append(key, v);
       else urlSearchParams.append(key, value);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (Array.isArray(value))
for (const v in value) urlSearchParams.append(key, v);
if (Array.isArray(value))
for (const v of value) urlSearchParams.append(key, v);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/`[[...id]]/page.tsx around lines 23 - 24, The loop that appends
multi-valued query params uses `for (const v in value)` which iterates indices
not elements, corrupting values; update the loop in the code that builds
`urlSearchParams` to iterate array elements (use `for...of` or equivalent) so
`urlSearchParams.append(key, v)` receives the actual element values; locate the
block around `urlSearchParams.append` in the page rendering/redirect logic in
src/app/[[...id]]/page.tsx and replace the `for...in` with a `for...of` over
`value`.

Comment on lines +44 to 54
const BreadcrumbWrapper = (wrapperProps: { children: ReactNode }) => (
<Breadcrumb>
<BreadcrumbList>
{pages.map((page, index) => (
<React.Fragment key={page.title + page.href}>
{index > 0 && <BreadcrumbSeparator />}
<BreadcrumbItem>
{page.href && (
<BreadcrumbLink href={page.href}>{page.title}</BreadcrumbLink>
)}
{!page.href && <BreadcrumbPage>{page.title}</BreadcrumbPage>}
</BreadcrumbItem>
</React.Fragment>
))}
<BreadcrumbLink href="/">
<Home />
</BreadcrumbLink>
<Separator />
{wrapperProps.children} {isPending && <Spinner />}
</BreadcrumbList>
</Breadcrumb>
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Anti-pattern: component defined inside render causes remount on every render.

BreadcrumbWrapper is declared as a new function component on every render of Breadcrumbs. React sees a new component type each cycle, so it unmounts and remounts all children — destroying DOM and any child state (e.g., the Combobox open state, search input text).

Refactor to render the wrapper inline instead:

♻️ Proposed refactor — use inline JSX wrapping

Extract the common wrapper as plain JSX and use it directly in the two return paths:

-  const BreadcrumbWrapper = (wrapperProps: { children: ReactNode }) => (
-    <Breadcrumb>
-      <BreadcrumbList>
-        <BreadcrumbLink href="/">
-          <Home />
-        </BreadcrumbLink>
-        <Separator />
-        {wrapperProps.children} {isPending && <Spinner />}
-      </BreadcrumbList>
-    </Breadcrumb>
-  );
-
-  if (!resource)
-    return (
-      <BreadcrumbWrapper>
-        <BreadcrumbContent resource={{ id, name: id, subtype: "" }} />
-      </BreadcrumbWrapper>
-    );
+  const breadcrumbContent = !resource ? (
+    <BreadcrumbContent resource={{ id, name: id, subtype: "" }} />
+  ) : (
+    <>
+      {pages.map((page, index) => (
+        <React.Fragment key={page.id || index}>
+          {index > 0 && <Separator />}
+          <ChildrenSelect resource={page} items={page.parent?.children || []}>
+            <BreadcrumbContent resource={page} />
+          </ChildrenSelect>
+        </React.Fragment>
+      ))}
+      {resource.children.length > 0 && (
+        <>
+          <Separator />
+          <ChildrenSelect resource={resource} items={resource.children} className="opacity-70">
+            Select {resource.subtype === "consortium" ? "organization" : "repository"}...
+          </ChildrenSelect>
+        </>
+      )}
+    </>
+  );
+
+  return (
+    <Breadcrumb>
+      <BreadcrumbList>
+        <BreadcrumbLink href="/">
+          <Home />
+        </BreadcrumbLink>
+        <Separator />
+        {breadcrumbContent} {isPending && <Spinner />}
+      </BreadcrumbList>
+    </Breadcrumb>
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Breadcrumbs.tsx` around lines 44 - 54, BreadcrumbWrapper is
currently defined inside the Breadcrumbs render which creates a new component
type each render causing remounts; remove the inner function and instead render
the wrapper inline (or move it to a stable outer component) by replacing usages
of <BreadcrumbWrapper>{...}</BreadcrumbWrapper> with the equivalent JSX
structure using Breadcrumb, BreadcrumbList, BreadcrumbLink, Home, Separator and
conditionally render {isPending && <Spinner />}; ensure any props (children) are
forwarded directly and delete the BreadcrumbWrapper declaration so children like
the Combobox keep their state.

Comment on lines +110 to +120
const BreadcrumbPageLink = (wrapperProps: { children: ReactNode }) =>
props.resource.id === id ? (
<BreadcrumbPage {...wrapperProps} className={className} />
) : (
<BreadcrumbLink
{...wrapperProps}
href={`/${props.resource.id}`}
className={className}
onMouseDown={(e) => e.stopPropagation()}
/>
);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Same anti-pattern: BreadcrumbPageLink component defined inside render.

BreadcrumbPageLink is redefined on every render of BreadcrumbContent, causing the same remount problem. Since it captures closure variables (props.resource.id, id, className), convert it to a conditional JSX expression or extract it as a separate component that receives these as props.

♻️ Proposed refactor — inline the conditional
   return (
     <BreadcrumbItem>
-      <BreadcrumbPageLink>
+      {props.resource.id === id ? (
+        <BreadcrumbPage className={className}>
+          <Item className="p-0 pt-5">
+            <ItemContent className="gap-0">
+              <ItemTitle className={props.resource.id === id ? "bg-black/0 font-semibold" : ""}>
+                {props.resource.name} <ResourceBadge resource={props.resource} />
+              </ItemTitle>
+              <ItemDescription className="text-muted-foreground/75 text-start">
+                {props.resource.id}
+              </ItemDescription>
+            </ItemContent>
+          </Item>
+        </BreadcrumbPage>
+      ) : (
+        <BreadcrumbLink
+          href={`/${props.resource.id}`}
+          className={className}
+          onMouseDown={(e) => e.stopPropagation()}
+        >
-        <Item className="p-0 pt-5">
-          <ItemContent className="gap-0">
-            <ItemTitle
-              className={
-                props.resource.id === id ? "bg-black/0 font-semibold" : ""
-              }
-            >
-              {props.resource.name} <ResourceBadge resource={props.resource} />
-            </ItemTitle>
-            <ItemDescription className="text-muted-foreground/75 text-start">
-              {props.resource.id}
-            </ItemDescription>
-          </ItemContent>
-        </Item>
-      </BreadcrumbPageLink>
+          <Item className="p-0 pt-5">
+            <ItemContent className="gap-0">
+              <ItemTitle>
+                {props.resource.name} <ResourceBadge resource={props.resource} />
+              </ItemTitle>
+              <ItemDescription className="text-muted-foreground/75 text-start">
+                {props.resource.id}
+              </ItemDescription>
+            </ItemContent>
+          </Item>
+        </BreadcrumbLink>
+      )}
     </BreadcrumbItem>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/Breadcrumbs.tsx` around lines 110 - 120, BreadcrumbPageLink is
being redefined on every render inside BreadcrumbContent and closes over
props.resource.id, id, and className; extract the conditional into either (A) an
inline JSX expression in the render where you choose between <BreadcrumbPage ...
/> and <BreadcrumbLink ... /> using the existing condition (props.resource.id
=== id) or (B) move BreadcrumbPageLink out as a new pure component that accepts
propsResourceId, id, className, children and the onMouseDown handler, then
render that component from BreadcrumbContent so it no longer gets recreated each
render; ensure the onMouseDown={(e) => e.stopPropagation()} behavior is
preserved and pass through children to BreadcrumbPage/BreadcrumbLink.

Comment on lines +83 to +93
const registrationYears = doisMeta.registered || [];

return {
total: doisMeta.total,
resourceTypeData,
registrationYears,
registrationsData: registrationYears.reverse().map((f) => ({
year: f.id,
count: f.count,
})),
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

.reverse() mutates registrationYears in place — both returned fields share the same reversed array.

Array.prototype.reverse() mutates the original array. After line 89 executes, the registrationYears reference on line 88 points to the same (now reversed) array. This means the Combobox in FilterByRegistrationYear will display years in ascending order (oldest first) — which may or may not be intended. Use a non-mutating approach for clarity:

Proposed fix
   return {
     total: doisMeta.total,
     resourceTypeData,
     registrationYears,
-    registrationsData: registrationYears.reverse().map((f) => ({
+    registrationsData: [...registrationYears].reverse().map((f) => ({
       year: f.id,
       count: f.count,
     })),
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const registrationYears = doisMeta.registered || [];
return {
total: doisMeta.total,
resourceTypeData,
registrationYears,
registrationsData: registrationYears.reverse().map((f) => ({
year: f.id,
count: f.count,
})),
};
const registrationYears = doisMeta.registered || [];
return {
total: doisMeta.total,
resourceTypeData,
registrationYears,
registrationsData: [...registrationYears].reverse().map((f) => ({
year: f.id,
count: f.count,
})),
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/data/fetch.ts` around lines 83 - 93, The returned object mutates
registrationYears by calling registrationYears.reverse(), causing both
registrationYears and registrationsData to reference the same reversed array;
instead create a non-mutating copy before reversing (e.g., use a shallow copy
like [...registrationYears] or registrationYears.slice()) when building
registrationsData so registrationYears remains in its original order; update the
code that constructs registrationsData (the registrationsData mapping derived
from registrationYears) to operate on the copied-and-reversed array to avoid
in-place mutation of the doisMeta.registered value.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No data being returned for version from /completeness

2 participants