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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import './dist/dev/types/routes.d.ts';
import './dist/types/routes.d.ts';

// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
55 changes: 55 additions & 0 deletions src/components/Feedbacks/EmptyStatePanel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { render, screen } from '@/test-utils';
import { describe, expect, test } from 'vitest';
import { EmptyStatePanel } from './EmptyStatePanel';

describe('EmptyStatePanel', () => {
test('renders title and description', () => {
render(<EmptyStatePanel title="No citations yet" description="Papers that cite this work will appear here." />);

expect(screen.getByRole('region')).toBeInTheDocument();
expect(screen.getByText('No citations yet')).toBeInTheDocument();
expect(screen.getByText('Papers that cite this work will appear here.')).toBeInTheDocument();
});

test('renders primary action when provided', () => {
render(
<EmptyStatePanel
title="No citations yet"
description="Papers that cite this work will appear here."
primaryAction={{ label: 'Show in search results', href: '/search?q=citations(bibcode:test)' }}
/>,
);

const link = screen.getByRole('link', { name: 'Show in search results' });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', '/search?q=citations(bibcode:test)');
});

test('renders secondary action when provided', () => {
render(
<EmptyStatePanel
title="No references listed"
description="This paper does not have indexed references."
secondaryAction={{ label: 'Back to Abstract', href: '/abs/test/abstract' }}
/>,
);

const link = screen.getByRole('link', { name: 'Back to Abstract' });
expect(link).toBeInTheDocument();
expect(link).toHaveAttribute('href', '/abs/test/abstract');
});

test('renders both actions when provided', () => {
render(
<EmptyStatePanel
title="No citations yet"
description="Papers that cite this work will appear here."
primaryAction={{ label: 'Show in search results', href: '/search?q=test' }}
secondaryAction={{ label: 'Back to Abstract', href: '/abs/test/abstract' }}
/>,
);

expect(screen.getByRole('link', { name: 'Show in search results' })).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Back to Abstract' })).toBeInTheDocument();
});
});
60 changes: 60 additions & 0 deletions src/components/Feedbacks/EmptyStatePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Box, Button, Heading, Text, VStack, useColorModeValue } from '@chakra-ui/react';
import { SimpleLink } from '@/components/SimpleLink';
import { ReactElement } from 'react';

export interface EmptyStatePanelAction {
label: string;
href: string;
}

export interface EmptyStatePanelProps {
title: string;
description: string;
primaryAction?: EmptyStatePanelAction;
secondaryAction?: EmptyStatePanelAction;
}

export const EmptyStatePanel = ({
title,
description,
primaryAction,
secondaryAction,
}: EmptyStatePanelProps): ReactElement => {
const cardBg = useColorModeValue('white', 'gray.800');
const borderColor = useColorModeValue('gray.200', 'gray.700');

return (
<Box
bg={cardBg}
borderWidth="1px"
borderColor={borderColor}
borderRadius="md"
boxShadow="base"
p={6}
mt={4}
role="region"
aria-label={title}
>
<VStack spacing={4} align="start">
<Heading as="h2" size="md">
{title}
</Heading>
<Text color="gray.500">{description}</Text>
{(primaryAction || secondaryAction) && (
<VStack spacing={2} align="start" pt={2}>
{primaryAction && (
<Button as={SimpleLink} href={primaryAction.href} colorScheme="blue" size="sm">
{primaryAction.label}
</Button>
)}
{secondaryAction && (
<Button as={SimpleLink} href={secondaryAction.href} variant="outline" size="sm" colorScheme="gray">
{secondaryAction.label}
</Button>
)}
</VStack>
)}
</VStack>
</Box>
);
};
1 change: 1 addition & 0 deletions src/components/Feedbacks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './CustomInfoMessage';
export * from './EmptyStatePanel';
export * from './LoadingMessage';
export * from './StandardAlertMessage';
89 changes: 33 additions & 56 deletions src/pages/abs/[id]/citations.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,63 @@
import { Alert, AlertIcon } from '@chakra-ui/react';
import { AbstractRefList } from '@/components/AbstractRefList';
import { AbsLayout } from '@/components/Layout/AbsLayout';
import { useGetAbstractParams } from '@/lib/useGetAbstractParams';
import { NextPage } from 'next';
import { useRouter } from 'next/router';
import { path } from 'ramda';
import { ItemsSkeleton } from '@/components/ResultList/ItemsSkeleton';
import { useGetAbstract, useGetCitations } from '@/api/search/search';
import { IDocsEntity } from '@/api/search/types';
import { getCitationsParams } from '@/api/search/models';
import { useGetAbstract, useGetCitations } from '@/api/search/search';
import { AbstractRefList } from '@/components/AbstractRefList';
import { EmptyStatePanel, StandardAlertMessage } from '@/components/Feedbacks';
import { AbsLayout } from '@/components/Layout';
import { ItemsSkeleton } from '@/components/ResultList';
import { createAbsGetServerSideProps } from '@/lib/serverside/absCanonicalization';
import { NumPerPageType } from '@/types';
import { useGetAbstractParams } from '@/lib/useGetAbstractParams';
import { parseAPIError } from '@/utils/common/parseAPIError';

const CitationsPage: NextPage = () => {
const router = useRouter();
const {
data: abstractDoc,
error: abstractError,
isLoading: absLoading,
isFetching: absFetching,
} = useGetAbstract({ id: router.query.id as string });
const doc = path<IDocsEntity>(['docs', 0], abstractDoc);
const id = router.query.id as string;
const pageIndex = router.query.p ? parseInt(router.query.p as string) - 1 : 0;

const { data: abstractDoc, error: abstractError } = useGetAbstract({ id });
const doc = abstractDoc?.docs?.[0];

const { getParams, onPageChange, onPageSizeChange } = useGetAbstractParams(doc?.bibcode);
const { rows } = getParams();

// get the primary response from server (or cache)
const {
data,
isSuccess,
error: citationsError,
isLoading: citLoading,
isFetching: citFetching,
} = useGetCitations({ ...getParams(), start: pageIndex * rows });
isLoading,
isFetching,
} = useGetCitations({
...getParams(),
start: pageIndex * rows,
});

const isLoading = absLoading || absFetching || citLoading || citFetching;
const hasError = abstractError || citationsError;
const isEmpty = isSuccess && !isFetching && (!data?.docs || data.docs.length === 0);
const citationsParams = getCitationsParams(doc?.bibcode, 0, rows);

const handlePageSizeChange = (n: NumPerPageType) => {
onPageSizeChange(n);
};

return (
<AbsLayout doc={doc} titleDescription="Papers that cite" label="Citations">
{isLoading ? <ItemsSkeleton count={10} /> : null}
{(abstractError || citationsError) && (
<Alert status="error">
<AlertIcon />
{abstractError?.message || citationsError?.message}
</Alert>
{isLoading || isFetching ? <ItemsSkeleton count={10} /> : null}
{hasError && <StandardAlertMessage title={parseAPIError(hasError)} />}
{isEmpty && (
<EmptyStatePanel
title="No citations yet"
description="Papers that cite this work will appear here as they are indexed."
secondaryAction={{
label: 'Back to Abstract',
href: `/abs/${id}/abstract`,
}}
/>
)}
{isSuccess && (
{isSuccess && !isEmpty && (
<AbstractRefList
doc={doc}
docs={data.docs}
totalResults={data.numFound}
onPageChange={onPageChange}
pageSize={rows}
onPageSizeChange={handlePageSizeChange}
onPageSizeChange={onPageSizeChange}
searchLinkParams={citationsParams}
/>
)}
Expand All @@ -68,26 +68,3 @@ const CitationsPage: NextPage = () => {
export default CitationsPage;

export const getServerSideProps = createAbsGetServerSideProps('citations');
// export const getServerSideProps: GetServerSideProps = composeNextGSSP(async (ctx) => {
// try {
// const { id } = ctx.params as { id: string };
// const queryClient = new QueryClient();
// await queryClient.prefetchQuery({
// queryKey: searchKeys.citations({ bibcode: id, start: 0 }),
// queryFn: fetchSearch,
// meta: { params: getCitationsParams(id, 0) },
// });
// return {
// props: {
// dehydratedState: dehydrate(queryClient),
// },
// };
// } catch (err) {
// logger.error({ err, url: ctx.resolvedUrl }, 'Error fetching details');
// return {
// props: {
// pageError: parseAPIError(err),
// },
// };
// }
// });
66 changes: 26 additions & 40 deletions src/pages/abs/[id]/coreads.tsx
Original file line number Diff line number Diff line change
@@ -1,77 +1,63 @@
import { NextPage } from 'next';
import { useRouter } from 'next/router';
import { useGetAbstractParams } from '@/lib/useGetAbstractParams';
import { getCoreadsParams } from '@/api/search/models';
import { useGetAbstract, useGetCoreads } from '@/api/search/search';
import { AbstractRefList } from '@/components/AbstractRefList';
import { EmptyStatePanel, StandardAlertMessage } from '@/components/Feedbacks';
import { AbsLayout } from '@/components/Layout';
import { ItemsSkeleton } from '@/components/ResultList';
import { StandardAlertMessage } from '@/components/Feedbacks';
import { parseAPIError } from '@/utils/common/parseAPIError';
import { AbstractRefList } from '@/components/AbstractRefList';
import { useGetAbstract, useGetCoreads } from '@/api/search/search';
import { getCoreadsParams } from '@/api/search/models';
import { createAbsGetServerSideProps } from '@/lib/serverside/absCanonicalization';
import { NumPerPageType } from '@/types';
import { useGetAbstractParams } from '@/lib/useGetAbstractParams';
import { parseAPIError } from '@/utils/common/parseAPIError';

const CoreadsPage: NextPage = () => {
const router = useRouter();
const { data: abstractDoc } = useGetAbstract({ id: router.query.id as string });
const doc = abstractDoc?.docs?.[0];
const id = router.query.id as string;
const pageIndex = router.query.p ? parseInt(router.query.p as string) - 1 : 0;

const { data: abstractDoc } = useGetAbstract({ id });
const doc = abstractDoc?.docs?.[0];

const { getParams, onPageChange, onPageSizeChange } = useGetAbstractParams(doc?.bibcode);
const { rows } = getParams();

const { data, isSuccess, isLoading, isFetching, error, isError } = useGetCoreads({
...getParams(),
start: pageIndex * rows,
});
const coreadsParams = getCoreadsParams(doc?.bibcode, 0, rows);

const handlePageSizeChange = (n: NumPerPageType) => {
onPageSizeChange(n);
};
const isEmpty = isSuccess && !isFetching && (!data?.docs || data.docs.length === 0);
const coreadsParams = getCoreadsParams(doc?.bibcode, 0, rows);

return (
<AbsLayout doc={doc} titleDescription="Papers also read by those who read" label="Coreads">
{isLoading || isFetching ? <ItemsSkeleton count={10} /> : null}
{isError ? <StandardAlertMessage title={parseAPIError(error)} /> : null}
{isSuccess ? (
{isError && <StandardAlertMessage title={parseAPIError(error)} />}
{isEmpty && (
<EmptyStatePanel
title="No co-reads available"
description="Co-reads show papers frequently read alongside this one. Requires read activity data."
secondaryAction={{
label: 'Back to Abstract',
href: `/abs/${id}/abstract`,
}}
/>
)}
{isSuccess && !isEmpty && (
<AbstractRefList
doc={doc}
docs={data.docs}
totalResults={data.numFound}
onPageChange={onPageChange}
pageSize={rows}
onPageSizeChange={handlePageSizeChange}
onPageSizeChange={onPageSizeChange}
searchLinkParams={coreadsParams}
/>
) : null}
)}
</AbsLayout>
);
};

export default CoreadsPage;

export const getServerSideProps = createAbsGetServerSideProps('coreads');
// export const getServerSideProps: GetServerSideProps = composeNextGSSP(async (ctx) => {
// try {
// const { id } = ctx.params as { id: string };
// const queryClient = new QueryClient();
// await queryClient.prefetchQuery({
// queryKey: searchKeys.coreads({ bibcode: id, start: 0 }),
// queryFn: fetchSearch,
// meta: { params: getCoreadsParams(id, 0) },
// });
// return {
// props: {
// dehydratedState: dehydrate(queryClient),
// },
// };
// } catch (err) {
// logger.error({ err, url: ctx.resolvedUrl }, 'Error fetching details');
// return {
// props: {
// pageError: parseAPIError(err),
// },
// };
// }
// });
Loading
Loading