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 ( -
-
- { + onLimitChange(Number(val)); + onPageChange(1); + }} + options={pageSizeOptions.map((option) => ({ + value: String(option), + label: String(toPersianNum(option)), + }))} + className="w-24 shrink-0" + buttonClassName="h-10 rounded-2xl border-slate-200 bg-slate-50 font-medium text-slate-700 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200" + /> + + {toPersianNum(currentPage)} / {toPersianNum(totalPages)} + +
+ + {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 });