refactor(frontend): share list empty state card

This commit is contained in:
2026-04-29 12:16:59 +03:30
parent 013c78a46d
commit a2bc1aa91f
5 changed files with 120 additions and 92 deletions

View File

@@ -0,0 +1,25 @@
import type { LucideIcon } from "lucide-react";
interface EmptyStateCardProps {
icon: LucideIcon;
title: string;
description: string;
className?: string;
}
export default function EmptyStateCard({
icon: Icon,
title,
description,
className = "",
}: EmptyStateCardProps) {
return (
<div
className={`flex flex-1 flex-col justify-center rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900 ${className}`}
>
<Icon className="mx-auto mb-3 h-12 w-12 text-slate-300 dark:text-slate-700" />
<h3 className="text-lg font-medium text-slate-900 dark:text-white">{title}</h3>
<p className="mt-1 text-slate-500 dark:text-slate-400">{description}</p>
</div>
);
}

View File

@@ -16,6 +16,7 @@ import { getClients } from "../api/clients"
import CreateClientModal from "../components/CreateClientModal"
import EditClientModal from "../components/EditClientModal"
import DeleteClientModal from "../components/DeleteClientModal"
import EmptyStateCard from "../components/EmptyStateCard"
import FilterBar from "../components/FilterBar"
import { ListPageSkeleton } from "../components/ListPageSkeleton"
import { Button } from "../components/ui/button"
@@ -169,13 +170,11 @@ export default function Clients() {
) : (
<div className="flex flex-1 flex-col gap-6">
{clients.length === 0 ? (
<div className="flex flex-col flex-1 rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
<Building2 className="mx-auto mb-3 h-12 w-12 text-slate-300 dark:text-slate-700" />
<h3 className="text-lg font-medium text-slate-900 dark:text-white">{t.clients.noClients}</h3>
<p className="mt-1 text-slate-500 dark:text-slate-400">
{searchQuery ? t.clients.noClientsSearch : t.clients.noClientsAdd}
</p>
</div>
<EmptyStateCard
icon={Building2}
title={t.clients.noClients}
description={searchQuery ? t.clients.noClientsSearch : t.clients.noClientsAdd}
/>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 2xl:grid-cols-3">
{clients.map((client) => {

View File

@@ -10,6 +10,7 @@ import { ProjectEditModal } from "../components/projects/ProjectEditModal";
import { Pagination } from "../components/Pagination";
import { Plus, Archive, Building2, Pencil, Trash2, X } from "lucide-react";
import EmptyStateCard from "../components/EmptyStateCard";
import FilterBar from "../components/FilterBar";
import { ListPageSkeleton } from "../components/ListPageSkeleton";
import { Button } from "../components/ui/button";
@@ -329,11 +330,11 @@ export const Projects: React.FC = () => {
) : (
<div className="flex flex-1 flex-col gap-6">
{projects.length === 0 ? (
<div className="flex flex-col flex-1 rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
<Building2 className="mx-auto mb-3 h-12 w-12 text-slate-300 dark:text-slate-700" />
<h3 className="text-lg font-medium text-slate-900 dark:text-white">{t.projects?.emptyState || 'No projects found'}</h3>
<p className="mt-1 text-slate-500 dark:text-slate-400">{t.projects?.noProjectsSearch}</p>
</div>
<EmptyStateCard
icon={Building2}
title={t.projects?.emptyState || "No projects found"}
description={t.projects?.noProjectsSearch || t.projects?.emptyState || "No projects found"}
/>
) : (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 2xl:grid-cols-3">
{projects.map((project) => {

View File

@@ -4,6 +4,7 @@ import { Edit2, Plus, Tag as TagIcon, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { createTag, deleteTag, getTags, type Tag, updateTag } from "../api/tags";
import EmptyStateCard from "../components/EmptyStateCard";
import { useAppContext } from "../context/AppContext";
import { useWorkspace } from "../context/WorkspaceContext";
import { useTranslation } from "../hooks/useTranslation";
@@ -193,9 +194,16 @@ export default function Tags() {
</div>
{isLoading ? (
<ListPageSkeleton variant="dense-grid" />
<ListPageSkeleton variant="list" />
) : (
<div className="flex flex-1 flex-col gap-6">
{tags.length === 0 ? (
<EmptyStateCard
icon={TagIcon}
title={t.tags?.emptyState || "No tags found"}
description={searchQuery ? t.tags?.noTagsSearch : t.tags?.emptyState || "No tags found"}
/>
) : (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
{tags.map((tag) => {
const canDeleteTag = canDeleteWorkspaceResource({
@@ -203,33 +211,26 @@ export default function Tags() {
currentUserId: user?.id,
createdById: tag.created_by?.id,
});
return (
<Card key={tag.id} className="overflow-hidden shadow-sm dark:border-slate-700 dark:bg-slate-800">
<CardContent className="flex h-full flex-col gap-4 p-5">
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<Card
key={tag.id}
className="flex flex-col text-slate-800 shadow-sm dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100"
>
<CardContent className="flex h-full flex-col justify-between gap-4 px-5 py-4">
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="flex items-center gap-3">
<div
className="h-9 w-9 shrink-0 rounded-xl border border-slate-200 dark:border-slate-700"
className="h-9 w-9 shrink-0 rounded-lg border border-slate-200 dark:border-slate-700"
style={{ backgroundColor: tag.color || DEFAULT_COLOR }}
/>
<div className="min-w-0">
<CardTitle className="truncate text-base text-slate-900 dark:text-white">{tag.name}</CardTitle>
<CardTitle className="text-lg line-clamp-1">{tag.name}</CardTitle>
</div>
</div>
{(canEditTag || canDeleteTag) && (
<div className="flex shrink-0 items-center gap-1">
{canEditTag && (
<Button
variant="ghost"
size="icon"
onClick={() => openEditModal(tag)}
className="h-8 w-8 text-slate-400 hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
title={t.actions?.edit || "Edit"}
>
<Edit2 className="w-4 h-4" />
</Button>
)}
<div className="flex shrink-0 items-center gap-2">
{canDeleteTag && (
<Button
variant="ghost"
@@ -238,7 +239,19 @@ export default function Tags() {
className="h-8 w-8 text-slate-400 hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-900/20 dark:hover:text-red-400"
title={t.actions?.delete || "Delete"}
>
<Trash2 className="w-4 h-4" />
<Trash2 className="h-4 w-4" />
</Button>
)}
{canEditTag && (
<Button
variant="ghost"
size="icon"
onClick={() => openEditModal(tag)}
className="h-8 w-8 text-slate-400 hover:bg-blue-50 hover:text-blue-600 dark:hover:bg-blue-900/20 dark:hover:text-blue-400"
title={t.actions?.edit || "Edit"}
>
<Edit2 className="h-4 w-4" />
</Button>
)}
</div>
@@ -248,17 +261,8 @@ export default function Tags() {
</Card>
);
})}
{tags.length === 0 && (
<div className="col-span-full flex flex-col flex-1 rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
<TagIcon className="mx-auto mb-3 h-12 w-12 text-slate-300 dark:text-slate-700" />
<h3 className="text-lg font-medium text-slate-900 dark:text-white">{t.tags?.emptyState || "No tags found"}</h3>
<p className="mt-1 text-slate-500 dark:text-slate-400">
{searchQuery ? t.tags?.noTagsSearch : t.tags?.emptyState}
</p>
</div>
)}
</div>
<Pagination
currentPage={currentPage}

View File

@@ -11,6 +11,7 @@ import {
type WorkspaceRole,
} from '../lib/permissions';
import FilterBar from '../components/FilterBar';
import EmptyStateCard from '../components/EmptyStateCard';
import { ListPageSkeleton } from '../components/ListPageSkeleton';
import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
@@ -230,13 +231,11 @@ export default function Workspaces() {
})}
{workspaces.length === 0 && (
<div className="flex flex-col flex-1 rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
<LayoutDashboard className="mx-auto mb-3 h-12 w-12 text-slate-300 dark:text-slate-700" />
<h3 className="text-lg font-medium text-slate-900 dark:text-white">{t.workspace.noWorkspace}</h3>
<p className="mt-1 text-slate-500 dark:text-slate-400">
{searchQuery ? t.workspace.noWorkspaceSearch : t.workspace?.emptyState}
</p>
</div>
<EmptyStateCard
icon={LayoutDashboard}
title={t.workspace.noWorkspace}
description={searchQuery ? t.workspace.noWorkspaceSearch : t.workspace?.emptyState || t.workspace.noWorkspace}
/>
)}
</div>