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
173 changes: 66 additions & 107 deletions src/components/catalogs/CatalogCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import classNames from "classnames";
import { Children, ReactElement, ReactNode, useState } from "react";
import { MdCode, MdKeyboardArrowDown } from "react-icons/md";
import { useNavigate } from "react-router-dom";
import { useAnchoredElement } from "../../hooks/useAnchoredElement";
import { SqlQueryEmbed } from "../catalog/SqlQueryEmbed";
import { Button } from "../core/Button";
import { CardActionsMenu, CatalogCardAction } from "../ui/CardActionsMenu";
import { CardAnchorLink } from "../ui/CardAnchorLink";
import { AppTooltip } from "../ui/AppTooltip";
import { Card, CardAction, Field } from "../ui/Card";
import { originalDataCatalogLink } from "./catalogActions";

export type { CatalogCardAction };
export type { CardAction as CatalogCardAction };
export { Field };

export function getSourceLink(bibcode: string): string {
return `https://ui.adsabs.harvard.edu/abs/${bibcode}/abstract`;
Expand All @@ -31,14 +30,13 @@ export function CatalogCard({
}: {
title: string;
children: ReactNode;
actions?: CatalogCardAction[];
actions?: CardAction[];
originalDataSql?: string;
anchorId?: string;
className?: string;
variant?: "fields" | "block";
}): ReactElement {
const navigate = useNavigate();
const { ref, highlighted } = useAnchoredElement(anchorId ?? "");
const [originalDataOpen, setOriginalDataOpen] = useState(false);
const [originalDataMounted, setOriginalDataMounted] = useState(false);

Expand All @@ -55,121 +53,82 @@ export function CatalogCard({
setOriginalDataOpen(true);
}

const cardActions = actions ?? [];
const hasActions = cardActions.length > 0;
const hasHeaderControls = hasActions || Boolean(originalDataSql);

return (
<div
ref={anchorId ? ref : undefined}
id={anchorId}
className={classNames(
"rounded-lg border border-border bg-surface p-3",
anchorId && highlighted && "card-anchor-highlight",
className,
)}
const headerControls = originalDataSql ? (
<Button
type="button"
className="!p-1.5 cursor-pointer"
onClick={toggleOriginalData}
aria-label={
originalDataOpen ? "Hide original data" : "View original data"
}
aria-expanded={originalDataOpen}
>
<MdKeyboardArrowDown
className={classNames(
"size-5 text-muted transition-transform duration-300 ease-in-out motion-reduce:transition-none",
originalDataOpen && "rotate-180",
)}
aria-hidden
/>
</Button>
) : null;

const afterChildren =
originalDataSql && originalDataMounted ? (
<div
className={
hasHeaderControls || anchorId
? "group/card flex items-start justify-between gap-2 mb-2"
: "mb-2"
}
className={classNames(
"grid transition-[grid-template-rows] duration-300 ease-in-out motion-reduce:transition-none",
originalDataOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
)}
>
<h3 className="text-base font-semibold min-w-0 flex items-center gap-1.5">
{title}
{anchorId && <CardAnchorLink anchorId={anchorId} />}
</h3>
{hasHeaderControls ? (
<div className="flex shrink-0 items-center gap-0.5">
{originalDataSql ? (
<Button
type="button"
className="!p-1.5 cursor-pointer"
onClick={toggleOriginalData}
aria-label={
originalDataOpen ? "Hide original data" : "View original data"
}
aria-expanded={originalDataOpen}
>
<MdKeyboardArrowDown
className={classNames(
"size-5 text-muted transition-transform duration-300 ease-in-out motion-reduce:transition-none",
originalDataOpen && "rotate-180",
)}
aria-hidden
/>
</Button>
) : null}
{hasActions ? <CardActionsMenu actions={cardActions} /> : null}
</div>
) : null}
</div>
{variant === "fields" ? (
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-base">
{children}
</dl>
) : (
children
)}
{originalDataSql && originalDataMounted ? (
<div
className={classNames(
"grid transition-[grid-template-rows] duration-300 ease-in-out motion-reduce:transition-none",
originalDataOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]",
)}
>
<div className="min-h-0 overflow-hidden">
<div
className={classNames(
"transition-opacity duration-300 ease-in-out motion-reduce:transition-none",
originalDataOpen ? "opacity-100" : "opacity-0",
)}
>
<div className="mt-3 flex flex-col gap-3 border-t border-border pt-3">
<SqlQueryEmbed key={originalDataSql} sql={originalDataSql} />
<div className="flex justify-end">
<AppTooltip content="Open in data catalog">
<Button
type="button"
className="!p-1.5 cursor-pointer"
onClick={() =>
navigate(originalDataCatalogLink(originalDataSql))
}
aria-label="Open in data catalog"
>
<MdCode className="size-5 text-muted" aria-hidden />
</Button>
</AppTooltip>
</div>
<div className="min-h-0 overflow-hidden">
<div
className={classNames(
"transition-opacity duration-300 ease-in-out motion-reduce:transition-none",
originalDataOpen ? "opacity-100" : "opacity-0",
)}
>
<div className="mt-3 flex flex-col gap-3 border-t border-border pt-3">
<SqlQueryEmbed key={originalDataSql} sql={originalDataSql} />
<div className="flex justify-end">
<AppTooltip content="Open in data catalog">
<Button
type="button"
className="!p-1.5 cursor-pointer"
onClick={() =>
navigate(originalDataCatalogLink(originalDataSql))
}
aria-label="Open in data catalog"
>
<MdCode className="size-5 text-muted" aria-hidden />
</Button>
</AppTooltip>
</div>
</div>
</div>
</div>
) : null}
</div>
</div>
) : null;

return (
<Card
title={title}
actions={actions}
headerControls={headerControls}
afterChildren={afterChildren}
anchorId={anchorId}
className={className}
variant={variant}
>
{children}
</Card>
);
}

export function CatalogNoData(): ReactElement {
return <p className="col-span-2 text-muted text-base">No data available.</p>;
}

export function Field({
label,
children,
}: {
label: ReactNode;
children: ReactNode;
}): ReactElement {
return (
<>
<dt className="text-muted">{label}</dt>
<dd>{children}</dd>
</>
);
}

export function CatalogDetailSection({
title,
children,
Expand Down
87 changes: 87 additions & 0 deletions src/components/ui/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import classNames from "classnames";
import { ReactElement, ReactNode } from "react";
import { useAnchoredElement } from "../../hooks/useAnchoredElement";
import { CardActionsMenu, CardAction } from "./CardActionsMenu";
import { CardAnchorLink } from "./CardAnchorLink";

export type { CardAction };

export function Card({
title,
children,
actions,
headerControls,
afterChildren,
anchorId,
className,
variant = "fields",
}: {
title: string;
children: ReactNode;
actions?: CardAction[];
headerControls?: ReactNode;
afterChildren?: ReactNode;
anchorId?: string;
className?: string;
variant?: "fields" | "block";
}): ReactElement {
const { ref, highlighted } = useAnchoredElement(anchorId ?? "");
const cardActions = actions ?? [];
const hasActions = cardActions.length > 0;
const hasHeaderControls =
hasActions || Boolean(headerControls) || Boolean(anchorId);

return (
<div
ref={anchorId ? ref : undefined}
id={anchorId}
className={classNames(
"rounded-lg border border-border bg-surface p-3",
anchorId && highlighted && "card-anchor-highlight",
className,
)}
>
<div
className={
hasHeaderControls
? "group/card flex items-start justify-between gap-2 mb-2"
: "mb-2"
}
>
<h3 className="text-base font-semibold min-w-0 flex items-center gap-1.5">
{title}
{anchorId && <CardAnchorLink anchorId={anchorId} />}
</h3>
{hasHeaderControls ? (
<div className="flex shrink-0 items-center gap-0.5">
{headerControls}
{hasActions ? <CardActionsMenu actions={cardActions} /> : null}
</div>
) : null}
</div>
{variant === "fields" ? (
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 text-base">
{children}
</dl>
) : (
children
)}
{afterChildren}
</div>
);
}

export function Field({
label,
children,
}: {
label: ReactNode;
children: ReactNode;
}): ReactElement {
return (
<>
<dt className="text-muted">{label}</dt>
<dd>{children}</dd>
</>
);
}
14 changes: 7 additions & 7 deletions src/components/ui/CardActionsMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,18 @@ import { MdMoreVert } from "react-icons/md";
import classNames from "classnames";
import { Button } from "../core/Button";

type CatalogCardActionCommon = {
type CardActionCommon = {
title: string;
description?: string;
icon?: IconType;
};

export type CatalogCardAction =
| (CatalogCardActionCommon & { href: string; onClick?: never })
| (CatalogCardActionCommon & { onClick: () => void; href?: never });
export type CardAction =
| (CardActionCommon & { href: string; onClick?: never })
| (CardActionCommon & { onClick: () => void; href?: never });

interface CardActionsMenuProps {
actions: CatalogCardAction[];
actions: CardAction[];
}

const menuItemClassName =
Expand All @@ -24,7 +24,7 @@ const menuItemClassName =
function ActionMenuItemContent({
action,
}: {
action: CatalogCardAction;
action: CardAction;
}): ReactElement {
const Icon = action.icon;

Expand Down Expand Up @@ -77,7 +77,7 @@ export function CardActionsMenu({
return () => document.removeEventListener("mousedown", handlePointerDown);
}, [open]);

function runAction(action: CatalogCardAction): void {
function runAction(action: CardAction): void {
if ("onClick" in action && action.onClick) {
action.onClick();
}
Expand Down
Loading
Loading