Files
kandev/apps/web/components/integrations/integration-list-toolbar.tsx
2026-09-12 10:06:05 +02:00

167 lines
4.9 KiB
TypeScript

"use client";
import type { ReactNode } from "react";
import { Trans, useTranslation } from "react-i18next";
import { IconRefresh } from "@tabler/icons-react";
import { Button } from "@kandev/ui/button";
import { Input } from "@kandev/ui/input";
import { cn, formatRelativeTime } from "@/lib/utils";
import { controlSizingClassName } from "@kandev/ui/control-sizing";
export type IntegrationListToolbarProps = {
title: string;
titleControl?: ReactNode;
count: number;
loading: boolean;
lastFetchedAt: Date | null;
customQuery: string;
committedQuery: string;
onCustomQueryChange: (value: string) => void;
onCommitCustomQuery: () => void;
onRefresh: () => void;
filter: ReactNode;
queryPlaceholder: string;
titleTestId?: string;
queryTestId?: string;
refreshTestId?: string;
};
type RefreshControlsProps = Pick<
IntegrationListToolbarProps,
"loading" | "lastFetchedAt" | "onRefresh" | "refreshTestId"
> & { showUpdatedPrefix: boolean };
function RefreshControls({
loading,
lastFetchedAt,
onRefresh,
refreshTestId,
showUpdatedPrefix,
}: RefreshControlsProps) {
const { t } = useTranslation();
return (
<>
{lastFetchedAt && !loading ? (
<span className="min-w-0 truncate text-xs leading-5 text-muted-foreground">
{showUpdatedPrefix ? t("github:updated") : ""}
{formatRelativeTime(lastFetchedAt.toISOString())}
</span>
) : null}
<Button
variant="ghost"
size="icon"
className="cursor-pointer"
onClick={onRefresh}
disabled={loading}
title={t("github:refresh")}
data-testid={refreshTestId}
>
<IconRefresh className={cn("h-4 w-4", loading && "animate-spin")} />
</Button>
</>
);
}
function MobileToolbarStatus({ count, ...props }: RefreshControlsProps & { count: number }) {
const { t } = useTranslation();
return (
<div className="flex min-w-0 items-center justify-between gap-3 md:hidden">
<span
className="shrink-0 whitespace-nowrap text-xs leading-5 text-muted-foreground"
data-testid="integration-mobile-result-count"
>
{props.loading ? (
t("integrations:loadingResults")
) : (
<Trans
i18nKey="integrations:resultCount"
count={count}
components={{ count: <span className="font-medium tabular-nums text-foreground" /> }}
/>
)}
</span>
<div className="flex min-w-0 items-center justify-end gap-2">
<RefreshControls {...props} />
</div>
</div>
);
}
export function IntegrationListToolbar({
title,
titleControl,
count,
loading,
lastFetchedAt,
customQuery,
committedQuery,
onCustomQueryChange,
onCommitCustomQuery,
onRefresh,
filter,
queryPlaceholder,
titleTestId,
queryTestId,
refreshTestId,
}: IntegrationListToolbarProps) {
const { t } = useTranslation();
const dirty = customQuery !== committedQuery;
return (
<div className="flex shrink-0 flex-col gap-2 border-b px-4 py-2.5 sm:px-6 md:flex-row md:flex-wrap md:items-center md:gap-3">
<div className="flex min-w-0 items-center gap-2">
<div className="flex min-w-0 flex-1 items-baseline gap-2 md:flex-initial">
{titleControl ?? (
<h2 className="truncate text-sm font-semibold" data-testid={titleTestId}>
{title}
</h2>
)}
<span className="hidden text-xs tabular-nums text-muted-foreground md:inline">
{loading ? "…" : count}
</span>
</div>
</div>
{filter}
<div className="relative w-full md:min-w-[240px] md:flex-1">
<Input
value={customQuery}
onChange={(event) => onCustomQueryChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
onCommitCustomQuery();
}
}}
onBlur={() => {
if (dirty) onCommitCustomQuery();
}}
placeholder={queryPlaceholder}
className={controlSizingClassName("standard", "pr-20")}
data-testid={queryTestId}
/>
{dirty ? (
<span className="pointer-events-none absolute right-2 top-1/2 hidden -translate-y-1/2 text-[10px] uppercase tracking-wider text-muted-foreground sm:inline">
{t("github:pressEnter")}
</span>
) : null}
</div>
<MobileToolbarStatus
count={count}
loading={loading}
lastFetchedAt={lastFetchedAt}
onRefresh={onRefresh}
refreshTestId={refreshTestId}
showUpdatedPrefix
/>
<div className="ml-auto hidden items-center gap-2 md:flex">
<RefreshControls
loading={loading}
lastFetchedAt={lastFetchedAt}
onRefresh={onRefresh}
refreshTestId={refreshTestId}
showUpdatedPrefix
/>
</div>
</div>
);
}