From 9f68be2a9b588368bbdf779cd9f127cd7ea70eda Mon Sep 17 00:00:00 2001 From: miloschwartz Date: Tue, 30 Jun 2026 21:18:46 -0400 Subject: [PATCH] add empty state --- messages/en-US.json | 5 + .../resource-launcher/LauncherEmptyState.tsx | 118 ++++++++++++++++++ .../resource-launcher/LauncherGroupList.tsx | 36 +++++- .../resource-launcher/ResourceLauncher.tsx | 11 ++ 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 src/components/resource-launcher/LauncherEmptyState.tsx diff --git a/messages/en-US.json b/messages/en-US.json index d27196be2..289399e5c 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -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", diff --git a/src/components/resource-launcher/LauncherEmptyState.tsx b/src/components/resource-launcher/LauncherEmptyState.tsx new file mode 100644 index 000000000..193ffae78 --- /dev/null +++ b/src/components/resource-launcher/LauncherEmptyState.tsx @@ -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 ( +
+ {Array.from({ length: 4 }).map((_, index) => ( +
+
+
+
+
+
+
+
+
+ ))} +
+ ); +} + +function GhostResourceList() { + return ( +
+ {Array.from({ length: 3 }).map((_, index) => ( +
+
+
+
+
+
+
+ ))} +
+ ); +} + +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 ( +
+
+ {layout === "grid" ? ( + + ) : ( + + )} +
+
+
+ +
+
+

+ {isNoResults + ? t("resourceLauncherEmptyStateNoResultsTitle") + : t("resourceLauncherEmptyStateTitle")} +

+

+ {isNoResults + ? trimmedQuery + ? t( + "resourceLauncherEmptyStateNoResultsWithQuery", + { query: trimmedQuery } + ) + : t( + "resourceLauncherEmptyStateNoResultsDescription" + ) + : t("resourceLauncherEmptyStateDescription")} +

+
+ {isNoResults && onClearFilters ? ( + + ) : null} +
+
+ ); +} diff --git a/src/components/resource-launcher/LauncherGroupList.tsx b/src/components/resource-launcher/LauncherGroupList.tsx index c90c3a7cc..7ca7e745d 100644 --- a/src/components/resource-launcher/LauncherGroupList.tsx +++ b/src/components/resource-launcher/LauncherGroupList.tsx @@ -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; + 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(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 ( +
+ +
+ ); + } + + return ( + + ); + } + return (
{groups.map((group) => { diff --git a/src/components/resource-launcher/ResourceLauncher.tsx b/src/components/resource-launcher/ResourceLauncher.tsx index b09dea773..e5258d122 100644 --- a/src/components/resource-launcher/ResourceLauncher.tsx +++ b/src/components/resource-launcher/ResourceLauncher.tsx @@ -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} />