Files
erika/app/(ui)/admin/backend/client.tsx
T
2026-08-27 14:11:20 +00:00

170 lines
5.1 KiB
TypeScript

"use client";
import { useState } from "react";
import {
Card,
CardAction,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Spinner } from "@/components/ui/spinner";
import { Bomb, RefreshCw } from "lucide-react";
import { forceRefresh, refetchPlatform } from "./actions";
import { toast } from "sonner";
interface Platform {
name: string;
cachedCount: number | null;
}
function formatCount(n: number | null): string {
if (n === null) return "No data";
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
return n.toString();
}
export function BackendClient({ platforms }: { platforms: Platform[] }) {
const [data, setData] = useState<Platform[]>(platforms);
const [loading, setLoading] = useState<string | null>(null);
const [pathPrefix, setPathPrefix] = useState("");
const [reloadTarget, setReloadTarget] = useState<"all" | "path" | null>(null);
const handleRefetch = async (platform: string) => {
setLoading(platform);
try {
const result = await refetchPlatform(platform);
setData((prev) =>
prev.map((p) =>
p.name === platform ? { ...p, cachedCount: result.count } : p
)
);
toast.success(`${platform} refetched successfully`);
} catch {
toast.error(`Failed to refetch ${platform}`);
} finally {
setLoading(null);
}
};
const handleForceRefresh = async (
target: "all" | "path",
prefix: string | null,
) => {
setReloadTarget(target);
try {
await forceRefresh(prefix);
toast.success(
prefix ? `Reload sent for ${prefix}` : "Reload sent to all clients",
);
} catch (error) {
toast.error(error instanceof Error ? error.message : "Failed to send reload");
} finally {
setReloadTarget(null);
}
};
const normalizedPathPrefix = pathPrefix.trim();
const pathIsValid = normalizedPathPrefix.startsWith("/");
return (
<div className="flex flex-col gap-4">
<div className="grid gap-4 sm:grid-cols-2">
{data.map((p) => (
<Card key={p.name}>
<CardHeader>
<CardTitle className="text-sm font-medium capitalize">
{p.name}
</CardTitle>
<CardAction>
<Badge
variant={p.cachedCount !== null ? "default" : "secondary"}
>
{formatCount(p.cachedCount)}
</Badge>
</CardAction>
</CardHeader>
<CardContent>
<Button
variant="outline"
size="sm"
disabled={loading !== null}
onClick={() => handleRefetch(p.name)}
>
{loading === p.name ? (
<Spinner data-icon="inline-start" />
) : (
<RefreshCw data-icon="inline-start" />
)}
Refetch
</Button>
</CardContent>
</Card>
))}
</div>
<Card>
<CardHeader>
<CardTitle>Client reload</CardTitle>
<CardDescription>
Reload every connected client or only clients under a pathname prefix.
</CardDescription>
</CardHeader>
<CardContent>
<Button
variant="destructive"
disabled={reloadTarget !== null}
onClick={() => handleForceRefresh("all", null)}
>
{reloadTarget === "all" ? (
<Spinner data-icon="inline-start" />
) : (
<Bomb data-icon="inline-start" />
)}
Force reload all
</Button>
<FieldGroup>
<Field data-invalid={pathPrefix.length > 0 && !pathIsValid}>
<FieldLabel htmlFor="reload-path-prefix">Path prefix</FieldLabel>
<Input
id="reload-path-prefix"
value={pathPrefix}
onChange={(event) => setPathPrefix(event.target.value)}
placeholder="/admin"
aria-invalid={pathPrefix.length > 0 && !pathIsValid}
/>
<FieldDescription>
Reloads clients whose pathname starts with this value.
</FieldDescription>
</Field>
<Button
variant="destructive"
disabled={reloadTarget !== null || !pathIsValid}
onClick={() => handleForceRefresh("path", normalizedPathPrefix)}
>
{reloadTarget === "path" ? (
<Spinner data-icon="inline-start" />
) : (
<Bomb data-icon="inline-start" />
)}
Reload matching path
</Button>
</FieldGroup>
</CardContent>
</Card>
</div>
);
}