Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 15 additions & 0 deletions src/app/citations/[...doi]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { notFound } from "next/navigation";
import ActionButtons from "@/components/ActionButtons";
import Breadcrumbs from "@/components/Breadcrumbs";
import { fetchEntity } from "@/data/fetch";

export default async function Layout({
children,
}: LayoutProps<"citations">) {

return (
<>
{children}
</>
);
}
92 changes: 92 additions & 0 deletions src/app/citations/[...doi]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { redirect } from "next/navigation";
import * as Cards from "@/components/cards/Cards";
import OverviewCard from "@/components/cards/OverviewCard";
import { SectionHeader } from "@/components/datacite/Headings";
import {
fetchDoiRecord,
fetchEvents,
fetchDoisRecords,
fetchDois,
} from "@/data/fetch";
import DoiRegistrationsChart from "@/components/DoiRegistrationsChart";
import ResourceTypesChart from "@/components/ResourceTypesChart";
import EventFeed from "@/components/EventFeed";
import { Filter } from "lucide-react";
import { DoiRecordList } from "@/components/DoiRecordList";
import { H2 } from "@/components/datacite/Headings";

interface PageProps {
params: { doi: string };
}

export default async function Page({ params }: PageProps) {
const { doi } = await params;
const doi_id = doi.join("/");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, find and read the file
find . -name "page.tsx" -path "*citations*[...doi]*" 2>/dev/null | head -20

Repository: datacite/puli

Length of output: 136


🏁 Script executed:

# Try alternative search
fd -t f "page.tsx" | grep -i citations

Repository: datacite/puli

Length of output: 132


🏁 Script executed:

# Broader search for the citations directory
fd -t d "citations" | head -20

Repository: datacite/puli

Length of output: 102


🏁 Script executed:

# Read the actual file
cat -n src/app/citations/[...doi]/page.tsx

Repository: datacite/puli

Length of output: 4004


🏁 Script executed:

# Check if there are type definitions or imports that show the proper PageProps typing
rg -t ts -t tsx "PageProps" --context 3 | head -50

Repository: datacite/puli

Length of output: 84


🏁 Script executed:

# Check tsconfig or next.config to understand the project setup
cat tsconfig.json 2>/dev/null | head -50

Repository: datacite/puli

Length of output: 710


🏁 Script executed:

# Look for other route handlers to see how they type params
fd -t f "page.tsx" | xargs rg "interface.*PageProps" -B2 -A2 2>/dev/null | head -100

Repository: datacite/puli

Length of output: 341


🏁 Script executed:

# Search for Next.js PageProps type definitions in node_modules or types
find . -name "*.d.ts" -type f | xargs rg "PageProps" 2>/dev/null | head -20

Repository: datacite/puli

Length of output: 410


🏁 Script executed:

# Check package.json for Next.js version
cat package.json | rg '"next"|"version"' -A 1 -B 1

Repository: datacite/puli

Length of output: 199


🏁 Script executed:

# Check if PageProps is imported from somewhere or if it's just using a standard Next.js pattern
rg -t typescript "import.*PageProps|from.*PageProps" 2>/dev/null

Repository: datacite/puli

Length of output: 39


🏁 Script executed:

# Look at how other dynamic routes are typed
cat -n src/app/citationsByEntity/[id]/page.tsx | head -30

Repository: datacite/puli

Length of output: 1486


🏁 Script executed:

# Also check the root page to see the pattern
cat -n src/app/page.tsx | head -30

Repository: datacite/puli

Length of output: 1394


🏁 Script executed:

# Check if PageProps is globally defined or imported from somewhere
rg -l "PageProps" --type ts --type tsx 2>/dev/null | xargs grep -l "^import.*PageProps\|^export.*PageProps\|^type PageProps\|^interface PageProps" 2>/dev/null

Repository: datacite/puli

Length of output: 41


🏁 Script executed:

# Look for global type definitions in a types file or d.ts
fd -t f "\.d\.ts$|types\.ts" | xargs cat 2>/dev/null

Repository: datacite/puli

Length of output: 3203


🏁 Script executed:

# Check for Next.js global types or augmentations
rg "declare global|PageProps" -t typescript 2>/dev/null | head -20

Repository: datacite/puli

Length of output: 39


🏁 Script executed:

# Check if PageProps is coming from a global augmentation or middleware file
rg "PageProps" -B 5 -A 5 | head -80

Repository: datacite/puli

Length of output: 3064


🏁 Script executed:

# Look for any explicit imports of PageProps from next or types
rg "import.*PageProps" 2>/dev/null

Repository: datacite/puli

Length of output: 39


🏁 Script executed:

# Check if there's any global d.ts file that defines PageProps
fd "global\.d\.ts" -o fd "\.d\.ts$" | xargs cat 2>/dev/null | rg "PageProps" -B 2 -A 2

Repository: datacite/puli

Length of output: 180


🏁 Script executed:

# Check if there's a next-env.d.ts or similar that might declare PageProps globally
cat -n .next/types/app/page.ts 2>/dev/null || echo "File not found"

Repository: datacite/puli

Length of output: 71


🏁 Script executed:

# Search for any .d.ts files that might declare PageProps
find . -name "*.d.ts" -type f -exec cat {} \; 2>/dev/null | rg "PageProps" -B 2 -A 2

Repository: datacite/puli

Length of output: 39


🏁 Script executed:

# Check the package.json more thoroughly to understand the setup
cat package.json

Repository: datacite/puli

Length of output: 1408


🏁 Script executed:

# Check Next.js version and see if PageProps is a known type in Next.js 16
# Let's search the web for Next.js 16.0.10 PageProps typing
echo "Checking the actual Next.js typing pattern in 16.0.10..."

Repository: datacite/puli

Length of output: 113


🌐 Web query:

Next.js 16.0.10 PageProps type catch-all route params typing

💡 Result:

In Next.js 16.0.10 (App Router), PageProps is a global TypeScript helper type for typing page components with route-aware params and searchParams. For catch-all routes like app/shop/[...slug]/page.tsx, use PageProps with the route literal path. Example for catch-all route app/shop/[...slug]/page.tsx: tsx export default async function Page(props: PageProps<'/shop/[...slug]'>) { const { slug } = await props.params; // slug: string[] const searchParams = await props.searchParams; return

Path segments: {slug?.join('/')}
; } - params resolves to Promise<{ slug: string[] }> - For optional catch-all [[...slug]], it's Promise<{ slug: string[] | undefined }> - Types generate during next dev/build or run npx next typegen - Enable typedRoutes: true in next.config.ts for better IntelliSense - Must await params (async since Next.js 15+) Manual typing without PageProps: tsx export default async function Page({ params, }: { params: Promise<{ slug: string[] }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }>; }) { const { slug } = await params; // ... } No specific changes in 16.0.10; inherits Next.js 16 async params requirement.

Citations:


Fix the catch-all route typing before using join().

This is a [...doi] route, so doi is an array, not a string. The current interface makes doi.join("/") invalid against the page's own declared props, and Next.js 16 requires using PageProps<'/citations/[...doi]'> for route-aware params with proper typing.

🛠️ Suggested fix
-interface PageProps {
-  params: { doi: string };
-}
-
-export default async function Page({ params }: PageProps) {
+export default async function Page({
+  params,
+}: PageProps<"/citations/[...doi]">) {
   const { doi } = await params;
   const doi_id = Array.isArray(doi) ? doi.join("/") : doi;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/citations/`[...doi]/page.tsx around lines 18 - 24, The Page component
incorrectly types and awaits params: change the component signature to use
Next.js route-aware typing PageProps<'/citations/[...doi]'> so params.doi is
typed as string[], stop using await on params (params is not a promise), and
destructure/use params.doi as an array before calling join (e.g., const { doi }
= params; const doi_id = doi.join("/")). Update the Page function signature and
references to params/doi accordingly (look for PageProps, Page, params,
doi.join).


// Fetch the DOI record and events
const [record, eventsResult, doisRecords] = await Promise.all([
fetchDoiRecord(doi_id),
fetchEvents(doi_id),
fetchDoisRecords("reference_ids:" + doi_id),
]);
Comment on lines +27 to +31
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Map facet data into the chart's expected shape first.

ResourceTypesChart expects { type, count }[], but doisRecords.meta.resourceTypes comes through as Facet[] with { id, title, count }. Passing it straight through leaves the chart without the field it renders against.

📊 Suggested fix
   const events = eventsResult?.data || [];
+  const resourceTypeData = (doisRecords.meta.resourceTypes ?? []).map(
+    ({ title, count }: { title: string; count: number }) => ({
+      type: title,
+      count,
+    }),
+  );
 
   return (
@@
-            {doisRecords.meta.resourceTypes && <ResourceTypesChart data={doisRecords.meta.resourceTypes} />}
+            {resourceTypeData.length > 0 && (
+              <ResourceTypesChart data={resourceTypeData} />
+            )}

Also applies to: 85-85

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

In `@src/app/citations/`[...doi]/page.tsx around lines 27 - 31, The chart is
receiving Facet[] (objects with id, title, count) but ResourceTypesChart expects
an array of { type, count }; locate where doisRecords.meta.resourceTypes is
passed (e.g., the call site that renders <ResourceTypesChart ... /> and the
Promise.all that fetches fetchDoisRecords) and map the facets before passing
them — transform each facet { id, title, count } into { type: facet.id or
facet.title (choose the chart's key), count: facet.count }; apply the same
mapping where other facet charts are fed (the second occurrence noted around the
later render) so all chart props match the expected shape.

const citationsOverTime = record?.data?.attributes?.citationsOverTime || [];
let chartData: { year: string; count: number }[] = [];
if (citationsOverTime.length > 0) {
const yearNums = citationsOverTime.map((item: { year: string }) =>
parseInt(item.year, 10),
);
const minYear = Math.min(...yearNums);
const maxYear = new Date().getFullYear();
const yearToCount: Record<string, number> = {};
citationsOverTime.forEach((item: { year: string; total: number }) => {
yearToCount[item.year] = item.total;
});
for (let y = minYear; y <= maxYear; y++) {
chartData.push({
year: y.toString(),
count: yearToCount[y.toString()] ?? 0,
});
}
}
const events = eventsResult?.data || [];

return (
<>
<div className="mt-8 mb-0">
<H2 >
{record.data.attributes.titles[0].title}
</H2>
<div className="text-gray-500 font-semibold">
{record.data.attributes.doi}
</div>
</div>
<main className="w-full mx-auto flex flex-row items-start gap-8">
<div className="w-1/2">
<div className="bg-card text-card-foreground border p-8 shadow-sm">
<h3 className="text-xl font-bold mb-4">Citations Over Time</h3>
{record.data.attributes.citationCount > 0 && (
<div className="max-w-300">
<DoiRegistrationsChart data={chartData} />
</div>
) || (
<p>No citation data available.</p>
)}
</div>
<div className="bg-card text-card-foreground border p-8 shadow-sm mt-8">
<h3 className="text-xl font-bold">Event Feed</h3>
<EventFeed events={events} doi={doi_id} />
</div>
</div>
<div className="w-1/2">
<div className="bg-card text-card-foreground border p-8 shadow-sm">
<h3 className="text-xl font-bold mb-4">
Available Citation Records
</h3>
{doisRecords.meta.resourceTypes && <ResourceTypesChart data={doisRecords.meta.resourceTypes} />}
<DoiRecordList records={doisRecords.data} />
</div>
</div>
</main>
</>
);
}
13 changes: 13 additions & 0 deletions src/app/citationsByEntity/[id]/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { H2 } from "@/components/datacite/Headings";
import type { Entity } from "@/types";

export default function Header(props: { entity: Entity }) {
return (
<div className="gap-0 mt-8">
<H2 className={`text-4xl w-full`}>{props.entity.name}</H2>
<div className="text-gray-500 font-semibold">
{props.entity.id}
</div>
</div>
);
}
23 changes: 23 additions & 0 deletions src/app/citationsByEntity/[id]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { notFound } from "next/navigation";
import ActionButtons from "@/components/ActionButtons";
import Breadcrumbs from "@/components/Breadcrumbs";
import { fetchEntity } from "@/data/fetch";
import Header from "./Header";

export default async function Layout({
params,
children,
}: LayoutProps<"/[id]">) {
const { id } = await params;

// Check if entity exists
const entity = await fetchEntity(id);
if (!entity) notFound();

return (
<>
<Header entity={entity} />
{children}
</>
);
}
82 changes: 82 additions & 0 deletions src/app/citationsByEntity/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { redirect } from "next/navigation";
import * as Cards from "@/components/cards/Cards";
import OverviewCard from "@/components/cards/OverviewCard";
import { SectionHeader } from "@/components/datacite/Headings";
import { fetchDoisRecords, fetchEntity } from "@/data/fetch";
import { fetchEntityCitations } from "@/data/fetch";
import DoiRegistrationsChart from "@/components/DoiRegistrationsChart";
import { DoiRecordList } from "@/components/DoiRecordList";

export default async function Page({
params,
searchParams,
}: PageProps<"/[id]">) {
const { id } = await params;

const [doisRecords] = await Promise.all([
fetchEntityCitations("provider.id:" + id + " OR client_id:" + id),
]);
Comment on lines +16 to +18
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Normalize before fetching, and redirect back into the same route tree.

The redirect target drops the /citationsByEntity/ prefix, so an uppercase request like /citationsByEntity/FOO gets sent to /foo instead of /citationsByEntity/foo. I'd also move this normalization block ahead of the fetch on Lines 16-18 so uppercase URLs don't hit DataCite first.

↪️ Suggested fix
-    redirect(`/${id.toLowerCase()}?${urlSearchParams.toString()}`);
+    const query = urlSearchParams.toString();
+    redirect(
+      `/citationsByEntity/${id.toLowerCase()}${query ? `?${query}` : ""}`,
+    );

Also applies to: 43-55

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

In `@src/app/citationsByEntity/`[id]/page.tsx around lines 16 - 18, Move the
normalization/redirect logic to before any data fetching so uppercase IDs don't
hit DataCite; compute a normalizedId (e.g., id.toLowerCase()) at the top of the
page component, and if id !== normalizedId perform a server redirect back into
the same route tree using the normalized path (for example
redirect(`/citationsByEntity/${normalizedId}`) or the equivalent in your routing
API) instead of redirecting to `/${normalizedId}`; then call
fetchEntityCitations with the normalizedId (e.g.,
fetchEntityCitations("provider.id:" + normalizedId + " OR client_id:" +
normalizedId)); apply the same change to the other normalization block that
affects the logic around the code handling lines 43-55 so all fetches use the
normalized value.


const citationsOverTime = doisRecords?.meta?.citations || [];
let chartData: { year: string; count: number }[] = [];
if (citationsOverTime.length > 0) {
// Map citations to { year, count }
const mapped = citationsOverTime.map((item: { id: string; count: number }) => ({
year: item.id,
count: item.count,
}));
const yearNums = mapped.map((item) => parseInt(item.year, 10));
const minYear = Math.min(...yearNums);
const maxYear = new Date().getFullYear();
Comment on lines +28 to +30
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard against invalid year values that produce NaN.

If the API returns a non-numeric year string, parseInt returns NaN, causing Math.min(...yearNums) to return NaN and the subsequent loop to silently fail.

Proposed fix
-  const yearNums = mapped.map((item: { year: string; count: number }) => parseInt(item.year, 10));
-  const minYear = Math.min(...yearNums);
-  const maxYear = new Date().getFullYear();
+  const yearNums = mapped
+    .map((item: { year: string; count: number }) => parseInt(item.year, 10))
+    .filter((y) => !Number.isNaN(y));
+  if (yearNums.length === 0) {
+    // No valid years, leave chartData empty
+  } else {
+    const minYear = Math.min(...yearNums);
+    const maxYear = new Date().getFullYear();
+    const yearToCount: Record<string, number> = {};
+    mapped.forEach((item: { year: string; count: number }) => {
+      yearToCount[item.year] = item.count;
+    });
+    for (let y = minYear; y <= maxYear; y++) {
+      chartData.push({
+        year: y.toString(),
+        count: yearToCount[y.toString()] ?? 0,
+      });
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/citationsByEntity/`[id]/page.tsx around lines 28 - 30, The code
computing yearNums and minYear can produce NaN when parseInt returns NaN for
non-numeric mapped entries; update the logic that builds yearNums (used in
mapped.map and variable yearNums) to parse each item.year safely (e.g., Number
or parseInt) and filter out non-finite results before calling Math.min, then
compute minYear from the filtered array and fall back to a safe default (e.g.,
maxYear or new Date().getFullYear()) if the filtered array is empty; ensure
references to yearNums, mapped, minYear, and maxYear are adjusted accordingly so
the subsequent year loop never receives NaN values.

const yearToCount: Record<string, number> = {};
mapped.forEach((item) => {
yearToCount[item.year] = item.count;
});
for (let y = minYear; y <= maxYear; y++) {
chartData.push({
year: y.toString(),
count: yearToCount[y.toString()] ?? 0,
});
}
}

// Redirect to lowercased id if it contains uppercase letters
if (id !== id.toLowerCase()) {
const urlSearchParams = new URLSearchParams();
Object.entries(await searchParams).forEach(([key, value]) => {
if (!value) return;

if (Array.isArray(value))
for (const v of value) urlSearchParams.append(key, v);
else urlSearchParams.append(key, value);
});

redirect(`/${id.toLowerCase()}?${urlSearchParams.toString()}`);
}

const entity = await fetchEntity(id);
if (!entity) return null;

return (
<main className="">
<main className="w-full mx-auto flex flex-row items-start gap-8">
Comment on lines +60 to +62
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid nested <main> elements — use a single <main> landmark per page.

Nested <main> elements violate HTML semantics and can confuse assistive technologies. Replace the inner <main> with a <div> or <section>.

Proposed fix
   return (
-    <main className="">
-      <main className="w-full mx-auto flex flex-row items-start gap-8">
+    <main className="w-full mx-auto flex flex-row items-start gap-8">
         <div className="w-1/2">
           ...
         </div>
         <div className="w-1/2">
           ...
         </div>
-      </main>
     </main>
   );
📝 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
return (
<main className="">
<main className="w-full mx-auto flex flex-row items-start gap-8">
return (
<main className="w-full mx-auto flex flex-row items-start gap-8">
<div className="w-1/2">
...
</div>
<div className="w-1/2">
...
</div>
</main>
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/citationsByEntity/`[id]/page.tsx around lines 60 - 62, The JSX in the
page component contains nested <main> elements which breaks HTML semantics;
locate the returned markup in src/app/citationsByEntity/[id]/page.tsx (the page
component render/return) and replace the inner <main className="w-full mx-auto
flex flex-row items-start gap-8"> with a semantically appropriate container such
as a <div> or <section>, preserving the existing className and structure so only
the tag changes.

<div className="w-1/2">
<div className="bg-card text-card-foreground border p-8 shadow-sm">
<h3 className="text-xl font-bold mb-4">Citations By Record Publication Year</h3>
<div className="max-w-300">
<DoiRegistrationsChart data={chartData} />
</div>
</div>
</div>
<div className="w-1/2">
<div className="bg-card text-card-foreground border p-8 shadow-sm">
<h3 className="text-xl font-bold mb-4">Most Cited Records</h3>
<div className="max-w-300">
<DoiRecordList records={doisRecords.data} />
</div>
</div>
</div>
</main>
</main>
);
}
82 changes: 82 additions & 0 deletions src/components/DoiRecordList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from "react";
import Link from "next/link";

export type DoiRecord = {
id: string;
attributes: {
titles: { title: string }[];
doi: string;
descriptions?: { description: string }[];
types: { resourceTypeGeneral?: string };
citationCount?: number;
viewCount?: number;
downloadCount?: number;
};
};

type DoiRecordListProps = {
records: DoiRecord[];
};

export function DoiRecordList({ records }: DoiRecordListProps) {
if (!records || records.length === 0) {
return <p>No citations found.</p>;
}
return (
<div className="flex flex-col py-4">
{records.map((record, idx) => (
<div key={record.id} className="flex flex-col">
<DoiRecordItem record={record} />
{idx < records.length - 1 && (
<div className="w-full flex justify-center">
<div className="h-px w-5/6 bg-gray-300 my-2" />
</div>
)}
</div>
))}
</div>
);
}

type DoiRecordItemProps = {
record: DoiRecord;
};

export function DoiRecordItem({ record }: DoiRecordItemProps) {
return (
<Link
href={`/citations/${record.attributes.doi}`}
className="cursor-pointer"
scroll={false}
shallow
>
Comment on lines +50 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does Next.js 16 Link component support the shallow prop?

💡 Result:

No, the Next.js 16 Link component does not support the shallow prop in the App Router (the default and recommended router). The shallow prop was specific to the Pages Router, where it enabled changing the URL without re-running data fetching methods like getStaticProps or getServerSideProps. In Next.js 16, official App Router Link documentation lists props like href, replace, scroll, prefetch, and onNavigate, but not shallow. GitHub discussions confirm shallow routing is unavailable in the App Router since Next.js 13, with no restoration in version 16 release notes. The Pages Router documentation still mentions shallow, but Next.js 16 emphasizes the App Router, and Pages Router is legacy.

Citations:


🏁 Script executed:

# Check Next.js version in package.json
cat package.json | grep -A 2 '"next"'

Repository: datacite/puli

Length of output: 128


🏁 Script executed:

# Check directory structure to determine routing system
git ls-files | head -20
find . -type d -name "app" -o -type d -name "pages" | head -10

Repository: datacite/puli

Length of output: 515


🏁 Script executed:

# Check the actual file to see routing context
cat -n src/components/DoiRecordList.tsx | head -60

Repository: datacite/puli

Length of output: 1987


Remove the shallow prop — it was removed from next/link in Next.js 13+.

The shallow prop no longer exists on the Link component in the App Router. For client-side navigation without refetching server data, the App Router handles this differently through its caching mechanisms.

Proposed fix
         <Link
       href={`/citations/${record.attributes.doi}`}
       className="cursor-pointer"
       scroll={false}
-      shallow
     >
📝 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
<Link
href={`/citations/${record.attributes.doi}`}
className="cursor-pointer"
scroll={false}
shallow
>
<Link
href={`/citations/${record.attributes.doi}`}
className="cursor-pointer"
scroll={false}
>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DoiRecordList.tsx` around lines 50 - 55, The Link usage in
DoiRecordList.tsx passes a removed prop `shallow` — remove the `shallow`
attribute from the <Link> element that navigates to
`/citations/${record.attributes.doi}`; simply delete the `shallow` prop (keeping
existing href, className, and scroll as-is) and, if you need App Router-specific
client-side behavior later, use next/navigation hooks (e.g., useRouter/push or
router.replace) instead of `shallow`.

<div className="py-2">
<div className="font-bold text-[#243B54]">
{record.attributes.titles[0].title}
</div>
Comment on lines +57 to +59
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Guard against empty titles array to prevent runtime crash.

titles is a required array in the type, but the API could return an empty array. Accessing titles[0].title without a check will throw a TypeError.

Proposed fix
       <div className="font-bold text-[`#243B54`]">
-        {record.attributes.titles[0].title}
+        {record.attributes.titles[0]?.title ?? "Untitled"}
       </div>
📝 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
<div className="font-bold text-[#243B54]">
{record.attributes.titles[0].title}
</div>
<div className="font-bold text-[`#243B54`]">
{record.attributes.titles[0]?.title ?? "Untitled"}
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DoiRecordList.tsx` around lines 57 - 59, The JSX directly
reads record.attributes.titles[0].title which will throw if titles is empty; in
DoiRecordList update the render to guard against empty or missing titles (e.g.,
use optional chaining and a safe fallback like
record.attributes.titles?.[0]?.title || 'Untitled' or fall back to another
attribute) so the <div className="font-bold text-[`#243B54`]"> never tries to
access index 0 of an empty array; keep the change local to the component where
record.attributes.titles is referenced.

<div className="text-gray-500 font-semibold text-sm">
https://doi.org/{record.attributes.doi}
</div>
<div className="text-gray-500 text-sm mt-2">
{record.attributes.publicationYear} · {record.attributes.publisher} · via {record.attributes.agency && record.attributes.agency.charAt(0).toUpperCase() + record.attributes.agency.slice(1)}
</div>
Comment on lines +63 to +65
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle missing metadata fields to avoid rendering artifacts like "· via ".

When publicationYear, publisher, or agency are undefined, the current template renders orphaned separators or "via " with nothing following. Use conditional rendering or template only the fields that exist.

Proposed fix
-               <div className="text-gray-500 text-sm mt-2">
-            {record.attributes.publicationYear} · {record.attributes.publisher} · via {record.attributes.agency && record.attributes.agency.charAt(0).toUpperCase() + record.attributes.agency.slice(1)}
-            </div>
+      <div className="text-gray-500 text-sm mt-2">
+        {[
+          record.attributes.publicationYear,
+          record.attributes.publisher,
+          record.attributes.agency &&
+            `via ${record.attributes.agency.charAt(0).toUpperCase() + record.attributes.agency.slice(1)}`,
+        ]
+          .filter(Boolean)
+          .join(" · ")}
+      </div>
📝 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
<div className="text-gray-500 text-sm mt-2">
{record.attributes.publicationYear} · {record.attributes.publisher} · via {record.attributes.agency && record.attributes.agency.charAt(0).toUpperCase() + record.attributes.agency.slice(1)}
</div>
<div className="text-gray-500 text-sm mt-2">
{[
record.attributes.publicationYear,
record.attributes.publisher,
record.attributes.agency &&
`via ${record.attributes.agency.charAt(0).toUpperCase() + record.attributes.agency.slice(1)}`,
]
.filter(Boolean)
.join(" · ")}
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DoiRecordList.tsx` around lines 63 - 65, In DoiRecordList.tsx
fix the metadata line rendering by building the pieces from record.attributes
(publicationYear, publisher) into an array, filtering out falsy values, and
joining them with ' · ' so orphan separators don't appear; then separately
render the agency only when present (e.g., compute agencyLabel = agency ? 'via '
+ capitalize(record.attributes.agency) : undefined) and append it with a
preceding separator if needed—update the JSX that currently uses
{record.attributes.publicationYear} · {record.attributes.publisher} · via
{record.attributes.agency && ...} to use these conditional pieces so missing
fields produce no extraneous separators or trailing "via ".

<div className="text-gray-500 line-clamp-3 text-sm mt-2">
{record.attributes.descriptions?.[0]?.description}
</div>

<div className="text-gray-500 text-sm mt-2">
{record.attributes.types.resourceTypeGeneral ? (
<span className="font-semibold mr-2 bg-[#e6f0fa] text-[#003366] rounded-full px-3 py-1 text-xs inline-block">
{record.attributes.types.resourceTypeGeneral}
</span>
) : null}
<span className="align-right font-semibold mt-2 bg-gray-100 text-[#003366] rounded-sm px-3 py-1 text-xs inline-block">
Citations: <span className="font-bold mr-2">{record.attributes.citationCount} </span>
Views: <span className="font-bold mr-2">{record.attributes.viewCount} </span>
Downloads: <span className="font-bold mr-2">{record.attributes.downloadCount} </span>
Comment on lines +76 to +79
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Default missing metrics before rendering the badge.

These fields are optional in the local type, so records without metrics will render undefined in the UI.

🪄 Suggested fix
-          Citations: <span className="font-bold mr-2">{record.attributes.citationCount} </span>
-          Views: <span className="font-bold mr-2">{record.attributes.viewCount} </span>
-          Downloads: <span className="font-bold mr-2">{record.attributes.downloadCount} </span>
+          Citations: <span className="font-bold mr-2">{record.attributes.citationCount ?? 0} </span>
+          Views: <span className="font-bold mr-2">{record.attributes.viewCount ?? 0} </span>
+          Downloads: <span className="font-bold mr-2">{record.attributes.downloadCount ?? 0} </span>
📝 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
<span className="align-right font-semibold mt-2 bg-gray-100 text-[#003366] rounded-sm px-3 py-1 text-xs inline-block">
Citations: <span className="font-bold mr-2">{record.attributes.citationCount} </span>
Views: <span className="font-bold mr-2">{record.attributes.viewCount} </span>
Downloads: <span className="font-bold mr-2">{record.attributes.downloadCount} </span>
<span className="align-right font-semibold mt-2 bg-gray-100 text-[`#003366`] rounded-sm px-3 py-1 text-xs inline-block">
Citations: <span className="font-bold mr-2">{record.attributes.citationCount ?? 0} </span>
Views: <span className="font-bold mr-2">{record.attributes.viewCount ?? 0} </span>
Downloads: <span className="font-bold mr-2">{record.attributes.downloadCount ?? 0} </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/DoiRecordList.tsx` around lines 73 - 76, The UI renders
undefined when a record lacks metrics; update the rendering in DoiRecordList
(the JSX that references record.attributes.citationCount,
record.attributes.viewCount, and record.attributes.downloadCount) to default
missing values (e.g., 0 or "-" as your design) before rendering; either inline
with a nullish-coalescing fallback like record.attributes.citationCount ?? 0 or
extract a small helper (e.g., formatMetric(record, 'citationCount')) and use
that for all three fields so undefined values never appear in the badge.

</span>
</div>
</div>
</Link>
);
}
Loading
Loading