mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-02 10:34:55 +00:00
add empty state
This commit is contained in:
@@ -3573,6 +3573,11 @@
|
||||
"resourceLauncherViewAsAdmin": "View as Admin",
|
||||
"resourceLauncherUnlabeled": "Unlabeled",
|
||||
"resourceLauncherNoResourcesInGroup": "No resources in this group",
|
||||
"resourceLauncherEmptyStateTitle": "No Resources Available",
|
||||
"resourceLauncherEmptyStateDescription": "You don't have access to any resources yet. Contact your administrator to request access.",
|
||||
"resourceLauncherEmptyStateNoResultsTitle": "No Resources Found",
|
||||
"resourceLauncherEmptyStateNoResultsDescription": "No resources match your current search or filters. Try adjusting them to find what you are looking for.",
|
||||
"resourceLauncherEmptyStateNoResultsWithQuery": "No resources match \"{query}\". Try adjusting your search or clearing filters to see all resources.",
|
||||
"resourceLauncherCopiedToClipboard": "Copied to clipboard",
|
||||
"resourceLauncherCopiedAccessDescription": "Resource access has been copied to your clipboard.",
|
||||
"resourceLauncherViewNamePlaceholder": "View name",
|
||||
|
||||
118
src/components/resource-launcher/LauncherEmptyState.tsx
Normal file
118
src/components/resource-launcher/LauncherEmptyState.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import { cn } from "@app/lib/cn";
|
||||
import { LayoutGrid, SearchX } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
type LauncherEmptyStateVariant = "empty" | "noResults";
|
||||
|
||||
type LauncherEmptyStateProps = {
|
||||
variant: LauncherEmptyStateVariant;
|
||||
layout: "grid" | "list";
|
||||
query?: string;
|
||||
onClearFilters?: () => void;
|
||||
};
|
||||
|
||||
function GhostResourceGrid() {
|
||||
return (
|
||||
<div
|
||||
className="grid w-full grid-cols-1 gap-2.5 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 [&>*]:min-w-0"
|
||||
aria-hidden
|
||||
>
|
||||
{Array.from({ length: 4 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex min-w-0 flex-col gap-2.5 rounded-xl border border-border/60 bg-muted/20 p-4"
|
||||
>
|
||||
<div className="flex items-center gap-5">
|
||||
<div className="size-10 shrink-0 rounded-lg bg-muted/60" />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<div className="h-3.5 w-3/5 rounded bg-muted/60" />
|
||||
<div className="h-3 w-2/5 rounded bg-muted/40" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GhostResourceList() {
|
||||
return (
|
||||
<div className="flex w-full flex-col" aria-hidden>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={cn(
|
||||
"flex items-center gap-4 px-4 py-3",
|
||||
index < 2 && "border-b border-border/60"
|
||||
)}
|
||||
>
|
||||
<div className="size-8 shrink-0 rounded-lg bg-muted/60" />
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
||||
<div className="h-3.5 w-2/5 rounded bg-muted/60" />
|
||||
<div className="h-3 w-1/4 rounded bg-muted/40" />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function LauncherEmptyState({
|
||||
variant,
|
||||
layout,
|
||||
query,
|
||||
onClearFilters
|
||||
}: LauncherEmptyStateProps) {
|
||||
const t = useTranslations();
|
||||
const isNoResults = variant === "noResults";
|
||||
const Icon = isNoResults ? SearchX : LayoutGrid;
|
||||
const trimmedQuery = query?.trim();
|
||||
|
||||
return (
|
||||
<div className="relative w-full overflow-hidden rounded-xl border border-dashed border-border">
|
||||
<div className="pointer-events-none absolute inset-0 opacity-50">
|
||||
{layout === "grid" ? (
|
||||
<GhostResourceGrid />
|
||||
) : (
|
||||
<GhostResourceList />
|
||||
)}
|
||||
</div>
|
||||
<div className="relative flex min-h-56 flex-col items-center justify-center gap-4 px-6 py-12 text-center">
|
||||
<div className="flex size-12 items-center justify-center rounded-full bg-muted">
|
||||
<Icon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
<div className="max-w-md space-y-1.5">
|
||||
<h3 className="text-base font-semibold text-foreground">
|
||||
{isNoResults
|
||||
? t("resourceLauncherEmptyStateNoResultsTitle")
|
||||
: t("resourceLauncherEmptyStateTitle")}
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isNoResults
|
||||
? trimmedQuery
|
||||
? t(
|
||||
"resourceLauncherEmptyStateNoResultsWithQuery",
|
||||
{ query: trimmedQuery }
|
||||
)
|
||||
: t(
|
||||
"resourceLauncherEmptyStateNoResultsDescription"
|
||||
)
|
||||
: t("resourceLauncherEmptyStateDescription")}
|
||||
</p>
|
||||
</div>
|
||||
{isNoResults && onClearFilters ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onClearFilters}
|
||||
>
|
||||
{t("clearAllFilters")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { LauncherEmptyState } from "./LauncherEmptyState";
|
||||
import { LauncherGroupSection } from "./LauncherGroupSection";
|
||||
|
||||
type LauncherGroupListProps = {
|
||||
@@ -23,15 +24,25 @@ type LauncherGroupListProps = {
|
||||
pageSize: number;
|
||||
};
|
||||
resourcesByGroupKey: Record<string, LauncherGroupResources>;
|
||||
onClearFilters?: () => void;
|
||||
};
|
||||
|
||||
function hasActiveLauncherFilters(config: LauncherViewConfig): boolean {
|
||||
return (
|
||||
config.query.trim().length > 0 ||
|
||||
config.siteIds.length > 0 ||
|
||||
config.labelIds.length > 0
|
||||
);
|
||||
}
|
||||
|
||||
export function LauncherGroupList({
|
||||
orgId,
|
||||
activeViewId,
|
||||
config,
|
||||
initialGroups,
|
||||
groupsPagination,
|
||||
resourcesByGroupKey
|
||||
resourcesByGroupKey,
|
||||
onClearFilters
|
||||
}: LauncherGroupListProps) {
|
||||
const loadMoreRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -55,7 +66,7 @@ export function LauncherGroupList({
|
||||
]
|
||||
);
|
||||
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
||||
const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isFetching } =
|
||||
useInfiniteQuery({
|
||||
...launcherQueries.groups(orgId, groupFilters),
|
||||
initialData: {
|
||||
@@ -91,6 +102,27 @@ export function LauncherGroupList({
|
||||
return () => observer.disconnect();
|
||||
}, [fetchNextPage, hasNextPage, isFetchingNextPage]);
|
||||
|
||||
if (groups.length === 0) {
|
||||
if (isFetching) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-16 text-muted-foreground">
|
||||
<Loader2 className="size-6 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LauncherEmptyState
|
||||
variant={
|
||||
hasActiveLauncherFilters(config) ? "noResults" : "empty"
|
||||
}
|
||||
layout={config.layout}
|
||||
query={config.query}
|
||||
onClearFilters={onClearFilters}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{groups.map((group) => {
|
||||
|
||||
@@ -306,6 +306,16 @@ export default function ResourceLauncher({
|
||||
[navigateToConfig]
|
||||
);
|
||||
|
||||
const handleClearFilters = useCallback(() => {
|
||||
setSearchInput("");
|
||||
navigateToConfig(activeViewIdRef.current, {
|
||||
...configRef.current,
|
||||
query: "",
|
||||
siteIds: [],
|
||||
labelIds: []
|
||||
});
|
||||
}, [navigateToConfig]);
|
||||
|
||||
const handleSaveToCurrent = () => {
|
||||
if (isDefaultView) {
|
||||
return;
|
||||
@@ -448,6 +458,7 @@ export default function ResourceLauncher({
|
||||
initialGroups={groups}
|
||||
groupsPagination={groupsPagination}
|
||||
resourcesByGroupKey={resourcesByGroupKey}
|
||||
onClearFilters={handleClearFilters}
|
||||
/>
|
||||
|
||||
<Credenza open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
|
||||
|
||||
Reference in New Issue
Block a user