diff --git a/src/components/ListPageSkeleton.tsx b/src/components/ListPageSkeleton.tsx
new file mode 100644
index 0000000..eb940dc
--- /dev/null
+++ b/src/components/ListPageSkeleton.tsx
@@ -0,0 +1,85 @@
+import { useMemo } from "react";
+
+import { useTranslation } from "../hooks/useTranslation";
+
+type ListPageSkeletonVariant = "list" | "standard-grid" | "dense-grid";
+
+type ListPageSkeletonProps = {
+ variant?: ListPageSkeletonVariant;
+};
+
+export function ListPageSkeleton({
+ variant = "standard-grid",
+}: ListPageSkeletonProps) {
+ const { t } = useTranslation();
+
+ const cardCount = variant === "list" ? 5 : variant === "dense-grid" ? 8 : 6;
+ const items = useMemo(
+ () => Array.from({ length: cardCount }, (_, index) => index),
+ [cardCount],
+ );
+
+ const gridClassName =
+ variant === "dense-grid"
+ ? "grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
+ : "grid grid-cols-1 gap-4 md:grid-cols-2 2xl:grid-cols-3";
+
+ return (
+
+ {variant === "list" ? (
+
+ {items.map((item) => (
+
+ ))}
+
+ ) : (
+
+ {items.map((item) => (
+
+ ))}
+
+ )}
+
+ );
+}
diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx
index 182946e..28e9a00 100644
--- a/src/components/Pagination.tsx
+++ b/src/components/Pagination.tsx
@@ -1,7 +1,9 @@
-import React from 'react';
-import { useTranslation } from '../hooks/useTranslation';
-import { Select } from './ui/Select';
-import { Button } from './ui/button';
+import React, { useMemo } from 'react';
+import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
+import { useTranslation } from '../hooks/useTranslation';
+import { cn } from '../lib/utils';
+import { Select } from './ui/Select';
+import { Button } from './ui/button';
interface PaginationProps {
currentPage: number;
@@ -12,7 +14,7 @@ interface PaginationProps {
pageSizeOptions?: number[];
}
-export const Pagination: React.FC = ({
+export const Pagination: React.FC = ({
currentPage,
totalCount,
limit,
@@ -29,56 +31,121 @@ export const Pagination: React.FC = ({
return num.toString().replace(/\d/g, d => '۰۱۲۳۴۵۶۷۸۹'[d as any]);
};
- const totalPages = Math.ceil(totalCount / limit) || 1;
-
- if (totalCount === 0) return null;
-
- const startItem = ((currentPage - 1) * limit) + 1;
- const endItem = Math.min(currentPage * limit, totalCount);
-
- return (
-
-
-
-
-
-
-
- {t.pagination?.page || 'Page'} {toPersianNum(currentPage)} {t.pagination?.of || 'of'} {toPersianNum(totalPages)}
-
-
-
-
-
- );
-};
+ const totalPages = Math.ceil(totalCount / limit) || 1;
+
+ if (totalCount === 0) return null;
+
+ const startItem = ((currentPage - 1) * limit) + 1;
+ const endItem = Math.min(currentPage * limit, totalCount);
+ const pageItems = useMemo(() => {
+ if (totalPages <= 7) {
+ return Array.from({ length: totalPages }, (_, index) => index + 1);
+ }
+
+ const pages: Array = [1];
+ const start = Math.max(2, currentPage - 1);
+ const end = Math.min(totalPages - 1, currentPage + 1);
+
+ if (start > 2) {
+ pages.push("ellipsis-left");
+ }
+
+ for (let page = start; page <= end; page += 1) {
+ pages.push(page);
+ }
+
+ if (end < totalPages - 1) {
+ pages.push("ellipsis-right");
+ }
+
+ pages.push(totalPages);
+ return pages;
+ }, [currentPage, totalPages]);
+
+ return (
+
+
+
+
+
+
+ {t.pagination?.showing || 'Showing'} {toPersianNum(startItem)} {t.pagination?.to || '-'} {toPersianNum(endItem)} {t.pagination?.of || 'of'} {toPersianNum(totalCount)}
+
+
+
+
+
+ {pageItems.map((pageItem, index) =>
+ typeof pageItem === 'number' ? (
+
+ ) : (
+
+
+
+ ),
+ )}
+
+
+
+
+
+
+ {t.pagination?.page || 'Page'} {toPersianNum(currentPage)} {t.pagination?.of || 'of'} {toPersianNum(totalPages)}
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/pages/Clients.tsx b/src/pages/Clients.tsx
index b335764..5872e02 100644
--- a/src/pages/Clients.tsx
+++ b/src/pages/Clients.tsx
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react"
-import { Plus, Building2, Loader2, Pencil, Trash2 } from "lucide-react"
+import { Plus, Building2, Pencil, Trash2 } from "lucide-react"
import { toast } from "sonner"
import { useWorkspace } from "../context/WorkspaceContext"
import { useAppContext } from "../context/AppContext"
@@ -16,6 +16,7 @@ import CreateClientModal from "../components/CreateClientModal"
import EditClientModal from "../components/EditClientModal"
import DeleteClientModal from "../components/DeleteClientModal"
import FilterBar from "../components/FilterBar"
+import { ListPageSkeleton } from "../components/ListPageSkeleton"
import { Button } from "../components/ui/button"
import { Card, CardContent, CardTitle } from "../components/ui/card"
import { Pagination } from "../components/Pagination"
@@ -120,7 +121,8 @@ export default function Clients() {
}
return (
-
+
+
@@ -155,15 +157,11 @@ export default function Clients() {
{isLoading ? (
-
+
) : (
-
+
{clients.length === 0 ? (
-
+
{t.clients.noClients}
@@ -246,6 +244,7 @@ export default function Clients() {
)}
)}
+
{canCreateClient && (
{
return (
-
+
+
@@ -285,13 +287,11 @@ export const Projects: React.FC = () => {
{loading ? (
-
-
{t.projects?.loading || 'Loading...'}
-
+
) : (
-
+
{projects.length === 0 ? (
-
+
{t.projects?.emptyState || 'No projects found'}
@@ -380,9 +380,10 @@ export const Projects: React.FC = () => {
/>
)}
+
{/* Modals */}
- {canCreateProject && isCreateModalOpen && (
+ {canCreateProject && isCreateModalOpen && (
setIsCreateModalOpen(false)}
diff --git a/src/pages/Workspaces.tsx b/src/pages/Workspaces.tsx
index 50f4971..9fb8c4e 100644
--- a/src/pages/Workspaces.tsx
+++ b/src/pages/Workspaces.tsx
@@ -10,8 +10,9 @@ import {
canWorkspace,
type WorkspaceRole,
} from '../lib/permissions';
-import FilterBar from '../components/FilterBar';
-import { Button } from '../components/ui/button';
+import FilterBar from '../components/FilterBar';
+import { ListPageSkeleton } from '../components/ListPageSkeleton';
+import { Button } from '../components/ui/button';
import { Input } from '../components/ui/input';
import { Card, CardContent, CardTitle } from '../components/ui/card';
import { Pagination } from '../components/Pagination';
@@ -116,7 +117,8 @@ export default function Workspaces() {
};
return (
-
+
+
@@ -146,14 +148,10 @@ export default function Workspaces() {
{isLoading ? (
-
-
-
{t.workspace?.loading || 'Loading...'}
-
-
+
) : (
-
-
+
+
{workspaces.map((workspace) => {
const canDeleteWorkspace = canWorkspace(workspace.my_role, WORKSPACE_DELETE);
const canEditWorkspace = canWorkspace(workspace.my_role, WORKSPACE_EDIT);
@@ -220,7 +218,7 @@ export default function Workspaces() {
})}
{workspaces.length === 0 && (
-
+
{t.workspace?.emptyState || 'No workspaces found'}
@@ -237,10 +235,11 @@ export default function Workspaces() {
pageSizeOptions={[10, 20, 50]}
/>
- )}
-
- {deleteModal.workspace && (
-
+
+ {deleteModal.workspace && (
+ {
setDeleteModal({ isOpen: false, workspace: null });