This commit is contained in:
hosseintaromi 2026-01-08 17:36:52 +03:30
parent ef76defb28
commit bfd1ea72a5
35 changed files with 1492 additions and 546 deletions

View File

@ -0,0 +1,429 @@
# درخواست APIهای داشبورد
سلام تیم بک‌اند 👋
برای تکمیل صفحه داشبورد نیاز به APIهای زیر داریم. لطفاً این APIها را پیاده‌سازی کنید:
---
## 1. API آمار کلی داشبورد (Dashboard Overview Stats)
**Endpoint:** `GET /api/v1/admin/dashboard/stats`
**Description:** دریافت آمار کلی سیستم شامل سفارشات، درآمد، کاربران و محصولات
**Response:**
```json
{
"orders": {
"total": 1250,
"pending": 45,
"completed": 1100,
"cancelled": 105,
"today_count": 23,
"today_amount": 45000000
},
"revenue": {
"total": 2500000000,
"today": 45000000,
"this_month": 350000000,
"last_month": 320000000,
"growth_percentage": 9.4
},
"users": {
"total": 8500,
"active": 7200,
"new_today": 15,
"new_this_month": 450
},
"products": {
"total": 1250,
"active": 1100,
"low_stock": 25,
"out_of_stock": 8
}
}
```
---
## 2. API فروش ماهانه (Monthly Sales Chart)
**Endpoint:** `GET /api/v1/admin/dashboard/monthly-sales`
**Description:** دریافت آمار فروش به صورت ماهانه برای نمایش در چارت
**Query Parameters:**
- `months` (optional, default: 6) - تعداد ماه‌های گذشته که باید برگردانده شود
**Response:**
```json
{
"data": [
{
"month": "فروردین",
"month_number": 1,
"year": 1403,
"total_amount": 350000000,
"order_count": 450
},
{
"month": "اردیبهشت",
"month_number": 2,
"year": 1403,
"total_amount": 420000000,
"order_count": 520
}
// ... برای تعداد ماه‌های درخواستی
]
}
```
**Note:** داده‌ها باید به ترتیب زمانی (از قدیمی‌ترین به جدیدترین) مرتب باشند.
---
## 3. API روند رشد (Growth Trend)
**Endpoint:** `GET /api/v1/admin/dashboard/growth-trend`
**Description:** دریافت روند رشد سیستم در بازه‌های زمانی مختلف
**Query Parameters:**
- `period` (optional, default: "monthly") - نوع بازه زمانی: `"daily"`, `"weekly"`, `"monthly"`
- `days` (optional, default: 30) - تعداد روزها برای `period=daily`
- `weeks` (optional, default: 12) - تعداد هفته‌ها برای `period=weekly`
- `months` (optional, default: 12) - تعداد ماه‌ها برای `period=monthly`
**Response:**
```json
{
"period": "monthly",
"data": [
{
"date": "1403/01",
"revenue": 350000000,
"orders": 450,
"users": 120
},
{
"date": "1403/02",
"revenue": 420000000,
"orders": 520,
"users": 135
}
// ...
],
"growth_rate": 9.4
}
```
---
## 4. API کاربران اخیر (Recent Users)
**Endpoint:** `GET /api/v1/admin/dashboard/recent-users`
**Description:** دریافت لیست کاربران جدید که اخیراً ثبت‌نام کرده‌اند
**Query Parameters:**
- `limit` (optional, default: 10) - تعداد کاربران
**Response:**
```json
{
"users": [
{
"id": 1,
"first_name": "علی",
"last_name": "احمدی",
"username": "ali_ahmadi",
"email": "ali@example.com",
"phone": "09123456789",
"status": "active",
"created_at": "2024-01-15T10:30:00Z"
},
{
"id": 2,
"first_name": "فاطمه",
"last_name": "حسینی",
"username": "fateme_hosseini",
"email": "fateme@example.com",
"phone": "09123456790",
"status": "active",
"created_at": "2024-01-14T15:20:00Z"
}
// ...
],
"total": 10
}
```
**Note:** کاربران باید به ترتیب تاریخ ثبت‌نام (جدیدترین اول) مرتب باشند.
---
## 5. API توزیع دستگاه‌های کاربری (Device Distribution)
**Endpoint:** `GET /api/v1/admin/dashboard/device-distribution`
**Description:** دریافت آمار توزیع دستگاه‌های کاربری (دسکتاپ، موبایل، تبلت)
**Response:**
```json
{
"data": [
{
"device_type": "desktop",
"label": "دسکتاپ",
"count": 4500,
"percentage": 45.5
},
{
"device_type": "mobile",
"label": "موبایل",
"count": 3500,
"percentage": 35.2
},
{
"device_type": "tablet",
"label": "تبلت",
"count": 2000,
"percentage": 19.3
}
],
"total": 10000
}
```
**Note:** درصدها باید به صورت عدد صحیح (بدون اعشار) برگردانده شوند.
---
## 6. API سفارش‌های اخیر (Recent Orders)
**Endpoint:** `GET /api/v1/admin/dashboard/recent-orders`
**Description:** دریافت لیست آخرین سفارش‌های ثبت شده
**Query Parameters:**
- `limit` (optional, default: 10) - تعداد سفارش‌ها
**Response:**
```json
{
"orders": [
{
"id": 1234,
"order_number": "ORD-2024-001234",
"customer_name": "علی احمدی",
"customer_id": 45,
"total_amount": 2500000,
"status": "pending",
"payment_status": "paid",
"created_at": "2024-01-15T14:30:00Z"
},
{
"id": 1233,
"order_number": "ORD-2024-001233",
"customer_name": "فاطمه حسینی",
"customer_id": 46,
"total_amount": 1800000,
"status": "completed",
"payment_status": "paid",
"created_at": "2024-01-15T13:20:00Z"
}
// ...
],
"total": 10
}
```
**Note:** سفارش‌ها باید به ترتیب تاریخ ایجاد (جدیدترین اول) مرتب باشند.
---
## 7. API محصولات پرفروش (Top Selling Products)
**Endpoint:** `GET /api/v1/admin/dashboard/top-products`
**Description:** دریافت لیست محصولات پرفروش
**Query Parameters:**
- `limit` (optional, default: 5) - تعداد محصولات
- `period` (optional, default: "month") - بازه زمانی: `"day"`, `"week"`, `"month"`
**Response:**
```json
{
"products": [
{
"id": 45,
"name": "محصول نمونه",
"sku": "PRD-001",
"sold_count": 250,
"revenue": 125000000,
"image_url": "https://example.com/images/product-45.jpg"
},
{
"id": 78,
"name": "محصول دیگر",
"sku": "PRD-002",
"sold_count": 180,
"revenue": 90000000,
"image_url": "https://example.com/images/product-78.jpg"
}
// ...
]
}
```
**Note:** محصولات باید به ترتیب تعداد فروش (بیشترین اول) مرتب باشند.
---
## 8. API آمار مقایسه‌ای (Comparison Stats)
**Endpoint:** `GET /api/v1/admin/dashboard/comparison`
**Description:** دریافت آمار مقایسه‌ای بین دوره فعلی و دوره قبلی
**Query Parameters:**
- `compare_with` (optional, default: "last_period") - نوع مقایسه: `"last_period"`, `"last_year"`
- `period` (optional, default: "month") - نوع دوره: `"day"`, `"week"`, `"month"`
**Response:**
```json
{
"current": {
"revenue": 350000000,
"orders": 450,
"users": 120
},
"previous": {
"revenue": 320000000,
"orders": 410,
"users": 105
},
"changes": {
"revenue": {
"amount": 30000000,
"percentage": 9.4,
"trend": "up"
},
"orders": {
"amount": 40,
"percentage": 9.8,
"trend": "up"
},
"users": {
"amount": 15,
"percentage": 14.3,
"trend": "up"
}
}
}
```
**Note:**
- `trend` می‌تواند `"up"`, `"down"` یا `"stable"` باشد
- درصدها می‌توانند اعشار داشته باشند
---
## نکات مهم و الزامات:
1. **احراز هویت:** همه APIها باید نیاز به احراز هویت داشته باشند (Bearer Token)
2. **فیلتر تاریخ:** در صورت امکان، همه APIها باید قابلیت فیلتر بر اساس بازه زمانی را داشته باشند:
- `from_date` (optional) - تاریخ شروع
- `to_date` (optional) - تاریخ پایان
3. **فرمت تاریخ:**
- تاریخ‌ها در Response باید به فرمت ISO 8601 برگردانده شوند: `"2024-01-15T14:30:00Z"`
- تاریخ‌های ورودی می‌توانند به فرمت ISO 8601 یا timestamp باشند
4. **فرمت مبالغ:**
- همه مبالغ به تومان هستند
- مبالغ باید به صورت عدد (integer) برگردانده شوند
5. **ترتیب داده‌ها:**
- داده‌های زمانی باید به ترتیب زمانی (از قدیمی‌ترین به جدیدترین) مرتب باشند
- داده‌های مرتب‌سازی شده (مثل پرفروش‌ترین) باید به ترتیب مناسب مرتب باشند
6. **مقدار پیش‌فرض:**
- در صورت نبود داده، آرایه خالی `[]` یا `null` برگردانده شود
- مقادیر عددی در صورت نبود داده باید `0` باشند
7. **خطاها:**
- در صورت خطا، کد HTTP مناسب برگردانده شود
- پیام خطا به فارسی و واضح باشد
8. **Performance:**
- برای بهینه‌سازی، می‌توان از کش استفاده کرد
- Queryها باید بهینه باشند تا زمان پاسخگویی کم باشد
---
## اولویت پیاده‌سازی:
1. **اولویت بالا:**
- API آمار کلی داشبورد (#1)
- API فروش ماهانه (#2)
- API کاربران اخیر (#4)
- API سفارش‌های اخیر (#6)
2. **اولویت متوسط:**
- API روند رشد (#3)
- API محصولات پرفروش (#7)
- API آمار مقایسه‌ای (#8)
3. **اولویت پایین:**
- API توزیع دستگاه‌های کاربری (#5) - در صورت وجود داده
---
## مثال استفاده:
```bash
# دریافت آمار کلی
curl -X GET "https://api.example.com/api/v1/admin/dashboard/stats" \
-H "Authorization: Bearer YOUR_TOKEN"
# دریافت فروش 6 ماه گذشته
curl -X GET "https://api.example.com/api/v1/admin/dashboard/monthly-sales?months=6" \
-H "Authorization: Bearer YOUR_TOKEN"
# دریافت کاربران اخیر
curl -X GET "https://api.example.com/api/v1/admin/dashboard/recent-users?limit=10" \
-H "Authorization: Bearer YOUR_TOKEN"
```
---
**ممنون می‌شوم اگر این APIها را در اسرع وقت پیاده‌سازی کنید تا بتوانیم داشبورد را کامل کنیم.** 🙏
**در صورت نیاز به توضیحات بیشتر یا تغییرات، لطفاً اطلاع دهید.**

View File

@ -93,9 +93,11 @@ const ProtectedRoute = ({ children }: { children: any }) => {
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<Layout>
<div className="flex items-center justify-center py-12">
<LoadingSpinner />
</div>
</Layout>
);
}
@ -206,9 +208,11 @@ const App = () => {
<AuthProvider>
<Router>
<Suspense fallback={
<div className="min-h-screen flex items-center justify-center">
<Layout>
<div className="flex items-center justify-center py-12">
<LoadingSpinner />
</div>
</Layout>
}>
<AppRoutes />
</Suspense>

View File

@ -28,10 +28,10 @@ export const StatsCard = ({
const isNegative = change && change < 0;
return (
<div className="card p-3 sm:p-4 lg:p-6 animate-fade-in">
<div className="card p-4 sm:p-5 lg:p-6 animate-fade-in">
<div className="flex items-center">
<div className="flex-shrink-0">
<div className={`p-2 sm:p-3 rounded-lg ${colorClasses[color as keyof typeof colorClasses] || colorClasses.blue}`}>
<div className={`p-3 sm:p-4 rounded-xl ${colorClasses[color as keyof typeof colorClasses] || colorClasses.blue} shadow-sm`}>
<Icon className="h-5 w-5 sm:h-6 sm:w-6 text-white" />
</div>
</div>

View File

@ -14,8 +14,8 @@ export const Header = ({ onMenuClick }: HeaderProps) => {
const [showUserMenu, setShowUserMenu] = useState(false);
return (
<header className="bg-white dark:bg-gray-800 shadow-sm border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between px-4 py-3">
<header className="bg-white dark:bg-gray-800 shadow-md border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center justify-between px-4 sm:px-6 lg:px-8 py-4">
<div className="flex items-center space-x-4 space-x-reverse">
<button
onClick={onMenuClick}

View File

@ -17,7 +17,7 @@ export const Layout = () => {
<Header onMenuClick={() => setSidebarOpen(true)} />
<main className="flex-1 overflow-y-auto bg-gray-50 dark:bg-gray-900">
<div className="min-h-full">
<div className="min-h-full py-6 px-4 sm:px-6 lg:px-8">
<Outlet />
</div>
</main>

View File

@ -1,5 +1,5 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { NavLink, useLocation } from 'react-router-dom';
import {
Home,
Settings,
@ -185,14 +185,58 @@ interface SidebarProps {
export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
const { user, logout } = useAuth();
const [expandedItems, setExpandedItems] = React.useState<string[]>([]);
const location = useLocation();
const [expandedItems, setExpandedItems] = React.useState<string[]>(() => {
// Load from localStorage on mount
const saved = localStorage.getItem('sidebar_expanded_items');
return saved ? JSON.parse(saved) : [];
});
// Auto-expand menu items based on current route
React.useEffect(() => {
const currentPath = location.pathname;
setExpandedItems(prev => {
const itemsToExpand: string[] = [];
menuItems.forEach(item => {
if (item.children) {
// Check if any child matches current path
const hasActiveChild = item.children.some(child => {
if (child.path) {
if (child.exact) {
return currentPath === child.path;
}
return currentPath.startsWith(child.path);
}
return false;
});
if (hasActiveChild && !prev.includes(item.title)) {
itemsToExpand.push(item.title);
}
}
});
if (itemsToExpand.length > 0) {
return [...prev, ...itemsToExpand];
}
return prev;
});
}, [location.pathname]);
React.useEffect(() => {
// Save to localStorage whenever expandedItems changes
localStorage.setItem('sidebar_expanded_items', JSON.stringify(expandedItems));
}, [expandedItems]);
const toggleExpanded = (title: string) => {
setExpandedItems(prev =>
prev.includes(title)
setExpandedItems(prev => {
const newItems = prev.includes(title)
? prev.filter(item => item !== title)
: [...prev, title]
);
: [...prev, title];
return newItems;
});
};
const renderMenuItem = (item: MenuItem, depth = 0) => {
@ -205,8 +249,8 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
<div key={item.title} className="space-y-1">
<button
onClick={() => toggleExpanded(item.title)}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors
text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700`}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200
text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:shadow-sm`}
style={{ paddingLeft: `${paddingLeft + 16}px` }}
>
<item.icon className="ml-3 h-5 w-5" />
@ -237,9 +281,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
}
}}
className={({ isActive }) =>
`w-full flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors ${isActive
? 'bg-primary-50 dark:bg-primary-900 text-primary-600 dark:text-primary-400'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white'
`w-full flex items-center px-4 py-3 text-sm font-medium rounded-xl transition-all duration-200 ${isActive
? 'bg-primary-50 dark:bg-primary-900 text-primary-600 dark:text-primary-400 shadow-sm'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-900 dark:hover:text-white hover:shadow-sm'
}`
}
style={{ paddingLeft: `${paddingLeft + 16}px` }}
@ -276,35 +320,35 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
w-64 transform transition-transform duration-300 ease-in-out
lg:translate-x-0 lg:block
${isOpen ? 'translate-x-0' : 'translate-x-full lg:translate-x-0'}
flex flex-col bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700
flex flex-col h-screen bg-white dark:bg-gray-800 border-l border-gray-200 dark:border-gray-700 shadow-lg lg:shadow-none
`}>
{/* Mobile close button */}
<div className="lg:hidden flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700">
<div className="lg:hidden flex justify-between items-center p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
<SectionTitle>
پنل مدیریت
</SectionTitle>
<button
onClick={onClose}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700"
className="p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
<X className="h-5 w-5 text-gray-600 dark:text-gray-400" />
</button>
</div>
{/* Logo - desktop only */}
<div className="hidden lg:flex h-16 items-center justify-center border-b border-gray-200 dark:border-gray-700">
<div className="hidden lg:flex h-16 items-center justify-center border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
<SectionTitle>
پنل مدیریت
</SectionTitle>
</div>
{/* Navigation */}
<nav className="flex-1 space-y-1 px-4 py-6 overflow-y-auto">
<nav className="flex-1 space-y-1 px-4 py-6 overflow-y-auto min-h-0">
{menuItems.map(item => renderMenuItem(item))}
</nav>
{/* User Info */}
<div className="border-t border-gray-200 dark:border-gray-700 p-4">
<div className="border-t border-gray-200 dark:border-gray-700 p-4 flex-shrink-0">
<div className="flex items-center space-x-3 space-x-reverse">
<div className="h-8 w-8 rounded-full bg-primary-600 flex items-center justify-center">
<span className="text-sm font-medium text-white">

View File

@ -23,7 +23,7 @@ export const Button = ({
className = '',
...rest
}: ButtonProps) => {
const baseClasses = 'inline-flex items-center justify-center rounded-lg font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2';
const baseClasses = 'inline-flex items-center justify-center rounded-xl font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 shadow-sm hover:shadow-md';
const variantClasses = {
primary: 'bg-primary-600 hover:bg-primary-700 text-white focus:ring-primary-500',

View File

@ -58,8 +58,8 @@ export const Modal = ({
<div className={`
relative w-full ${sizeClasses[size]}
bg-white dark:bg-gray-800 rounded-lg shadow-xl
transform transition-all
bg-white dark:bg-gray-800 rounded-2xl shadow-2xl
transform transition-all border border-gray-200 dark:border-gray-700
`}>
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-gray-200 dark:border-gray-700">
<SectionSubtitle>{title}</SectionSubtitle>

View File

@ -11,7 +11,7 @@ interface LabelProps extends TypographyProps {
// Page Headers
export const PageTitle = ({ children, className = '' }: TypographyProps) => (
<h1 className={`text-xl sm:text-2xl lg:text-3xl font-bold text-gray-900 dark:text-gray-100 ${className}`}>
<h1 className={`text-2xl sm:text-3xl font-bold text-gray-900 dark:text-gray-100 mb-6 ${className}`}>
{children}
</h1>
);
@ -109,7 +109,7 @@ export const FormHeader = ({ title, subtitle, backButton, actions, className = '
// Page Container with consistent mobile spacing
export const PageContainer = ({ children, className = '' }: TypographyProps) => (
<div className={`p-4 sm:p-6 lg:p-8 space-y-4 sm:space-y-6 max-w-none ${className}`}>
<div className={`space-y-6 max-w-none ${className}`}>
{children}
</div>
);

View File

@ -12,13 +12,14 @@ export const ThemeProvider = ({ children }: { children: any }) => {
useEffect(() => {
const savedTheme = localStorage.getItem('admin_theme') as 'light' | 'dark' | null;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const initialTheme = savedTheme || (prefersDark ? 'dark' : 'light');
const initialTheme = savedTheme || 'light';
setMode(initialTheme);
if (initialTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}, []);

View File

@ -59,7 +59,7 @@
@layer components {
.card {
@apply bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700;
@apply bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 transition-shadow duration-200 hover:shadow-lg;
}
.btn-primary {

View File

@ -6,7 +6,6 @@ import { Modal } from '../components/ui/Modal';
import { Pagination } from '../components/ui/Pagination';
import { UserForm } from '../components/forms/UserForm';
import { PermissionWrapper } from '../components/common/PermissionWrapper';
import { LoadingSpinner } from '../components/ui/LoadingSpinner';
import { TableColumn } from '../types';
import { UserFormData } from '../utils/validationSchemas';
import { useUsers, useCreateUser, useUpdateUser, useDeleteUser } from '../hooks/useUsers';
@ -211,7 +210,9 @@ const Users = () => {
</div>
{isLoading ? (
<LoadingSpinner />
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<Table columns={columns} data={[]} loading={true} />
</div>
) : (
<>
<div className="bg-white dark:bg-gray-800 rounded-lg overflow-hidden">

View File

@ -1,7 +1,6 @@
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowRight, Shield, Users, Key, Edit, Calendar, FileText, User } from 'lucide-react';
import { Button } from '../../../components/ui/Button';
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
import { useAdminUser } from '../core/_hooks';
import { PermissionWrapper } from '../../../components/common/PermissionWrapper';
import { PageContainer, PageTitle, SectionTitle, SectionSubtitle, BodyText } from '../../../components/ui/Typography';
@ -12,7 +11,47 @@ const AdminUserDetailPage = () => {
const { data: user, isLoading, error } = useAdminUser(id);
if (isLoading) return <LoadingSpinner />;
if (isLoading) {
return (
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="flex items-center justify-between">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded w-24"></div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{[...Array(3)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-4"></div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[...Array(4)].map((_, j) => (
<div key={j}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32"></div>
</div>
))}
</div>
</div>
))}
</div>
<div className="space-y-6">
{[...Array(2)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
<div className="space-y-3">
{[...Array(3)].map((_, j) => (
<div key={j} className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</PageContainer>
);
}
if (error) return <div className="text-red-600">خطا در بارگذاری اطلاعات کاربر</div>;
if (!user) return <div>کاربر یافت نشد</div>;

View File

@ -9,7 +9,6 @@ import { usePermissions } from '../../permissions/core/_hooks';
import { useRoles } from '../../roles/core/_hooks';
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { MultiSelectAutocomplete, Option } from "@/components/ui/MultiSelectAutocomplete";
import { ArrowRight } from "lucide-react";
import { FormHeader, PageContainer, Label } from '../../../components/ui/Typography';
@ -134,9 +133,19 @@ const AdminUserFormPage = () => {
if (isEdit && isLoadingUser) {
return (
<div className="flex justify-center items-center h-64">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(6)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}

View File

@ -3,7 +3,6 @@ import { useNavigate, useParams } from 'react-router-dom';
import { ArrowRight, FolderOpen } from 'lucide-react';
import { Button } from '../../../components/ui/Button';
import { Input } from '../../../components/ui/Input';
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
import { FileUploader } from '../../../components/ui/FileUploader';
import { useFileUpload, useFileDelete } from '../../../hooks/useFileUpload';
import { useToast } from '../../../contexts/ToastContext';
@ -116,9 +115,19 @@ const CategoryFormPage = () => {
if (isEdit && isLoadingCategory) {
return (
<div className="flex justify-center items-center h-64">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(4)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}

View File

@ -8,7 +8,6 @@ import { useDiscountCode, useCreateDiscountCode, useUpdateDiscountCode } from '.
import { CreateDiscountCodeRequest } from '../core/_models';
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { MultiSelectAutocomplete, Option } from "@/components/ui/MultiSelectAutocomplete";
import { SingleSelectAutocomplete } from "@/components/ui/SingleSelectAutocomplete";
import { JalaliDateTimePicker } from "@/components/ui/JalaliDateTimePicker";
@ -258,7 +257,23 @@ const DiscountCodeFormPage = () => {
}
};
if (isEdit && dcLoading) return <LoadingSpinner />;
if (isEdit && dcLoading) {
return (
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(6)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}
const isLoading = creating || updating;
return (

View File

@ -7,10 +7,10 @@ import { FileUploader } from "@/components/ui/FileUploader";
import { Button } from "@/components/ui/Button";
import { useLandingHero, useUpdateLandingHero } from "./core/_hooks";
import { LandingHeroData, HeroImage } from "./core/_models";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { PlusCircle, Trash2, Save } from "lucide-react";
import { useFileUpload, useFileDelete } from "@/hooks/useFileUpload";
import { Modal } from "@/components/ui/Modal";
import { PageContainer } from "@/components/ui/Typography";
const heroImageSchema = yup.object({
alt_text: yup.string().required("متن ALT الزامی است"),
@ -92,9 +92,19 @@ export const HeroSliderPage = () => {
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(5)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}

View File

@ -4,7 +4,6 @@ import { useOrder, useUpdateOrderStatus } from '../core/_hooks';
import { OrderStatus } from '../core/_models';
import { useShippingMethods } from '@/pages/shipping-methods/core/_hooks';
import { Button } from "@/components/ui/Button";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { Modal } from "@/components/ui/Modal";
import { PageContainer, PageTitle, SectionTitle } from "@/components/ui/Typography";
import {
@ -117,7 +116,41 @@ const OrderDetailPage = () => {
}
};
if (isLoading) return <LoadingSpinner />;
if (isLoading) {
return (
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-64"></div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{[...Array(3)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-4"></div>
<div className="space-y-3">
{[...Array(4)].map((_, j) => (
<div key={j} className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
))}
</div>
<div className="space-y-6">
{[...Array(2)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
<div className="space-y-3">
{[...Array(3)].map((_, j) => (
<div key={j} className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</PageContainer>
);
}
if (error || !order) {
return (
<PageContainer>

View File

@ -6,7 +6,6 @@ import { CreditCard } from 'lucide-react';
import { PageContainer, PageTitle } from '@/components/ui/Typography';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { usePaymentCard, useUpdatePaymentCard } from '../core/_hooks';
import { persianToEnglish } from '@/utils/numberUtils';
@ -141,9 +140,19 @@ const CardFormPage = () => {
if (isLoading) {
return (
<PageContainer>
<div className="flex justify-center items-center h-64">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(4)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
</PageContainer>
);
}

View File

@ -1,7 +1,6 @@
import React from 'react';
import { CreditCard, Loader2 } from 'lucide-react';
import { PageContainer, PageTitle } from '@/components/ui/Typography';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { useIPGStatus, useUpdateIPGStatus } from '../core/_hooks';
import { IPGStatus, IPG_LABELS } from '../core/_models';
@ -64,8 +63,32 @@ const IPGListPage = () => {
if (isLoading) {
return (
<PageContainer>
<div className="flex justify-center items-center h-64">
<LoadingSpinner />
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div>
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-56 mb-2 animate-pulse"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-64 animate-pulse"></div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="p-6">
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div
key={i}
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg animate-pulse"
>
<div className="flex-1">
<div className="flex items-center gap-3">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-32"></div>
<div className="h-5 bg-gray-200 dark:bg-gray-700 rounded w-16"></div>
</div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-48 mt-2"></div>
</div>
<div className="h-6 w-11 bg-gray-200 dark:bg-gray-700 rounded-full"></div>
</div>
))}
</div>
</div>
</div>
</PageContainer>
);

View File

@ -7,7 +7,6 @@ import { usePermission, useCreatePermission, useUpdatePermission } from '../core
import { PermissionFormData } from '../core/_models';
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { ArrowRight } from "lucide-react";
import { FormHeader, PageContainer, Label } from '../../../components/ui/Typography';
@ -81,9 +80,19 @@ const PermissionFormPage = () => {
if (isEdit && isLoadingPermission) {
return (
<div className="flex justify-center items-center h-64">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(4)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}

View File

@ -7,7 +7,6 @@ import { useProductOption, useCreateProductOption, useUpdateProductOption } from
import { ProductOptionFormData } from '../core/_models';
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { FileUploader } from "@/components/ui/FileUploader";
import { useFileUpload, useFileDelete } from "@/hooks/useFileUpload";
import { ArrowRight, Plus, Trash2 } from "lucide-react";
@ -142,9 +141,19 @@ const ProductOptionFormPage = () => {
if (isLoadingOption) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(4)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}

View File

@ -3,7 +3,6 @@ import { useState } from 'react';
import { Modal } from '../../../components/ui/Modal';
import { ArrowRight, Edit, Package, Tag, Image, Calendar, FileText, Eye, DollarSign, Hash, Layers, Settings } from 'lucide-react';
import { Button } from '../../../components/ui/Button';
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
import { useProduct } from '../core/_hooks';
import { PRODUCT_TYPE_LABELS } from '../core/_models';
import { PageContainer, PageTitle, SectionTitle } from '../../../components/ui/Typography';
@ -25,7 +24,44 @@ const ProductDetailPage = () => {
const [previewMedia, setPreviewMedia] = useState<{ url: string; type: 'image' | 'video' } | null>(null);
if (isLoading) return <LoadingSpinner />;
if (isLoading) {
return (
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="flex items-center justify-between">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded w-24"></div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{[...Array(4)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-4"></div>
<div className="space-y-3">
{[...Array(4)].map((_, j) => (
<div key={j} className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
))}
</div>
<div className="space-y-6">
{[...Array(3)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
<div className="space-y-3">
{[...Array(3)].map((_, j) => (
<div key={j} className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</PageContainer>
);
}
if (error) return <div className="text-red-600">خطا در بارگذاری اطلاعات محصول</div>;
if (!product) return <div>محصول یافت نشد</div>;

View File

@ -11,7 +11,6 @@ import { ProductFormData, ProductImage, ProductVariantFormData, PRODUCT_TYPES, P
import { MultiSelectAutocomplete } from "@/components/ui/MultiSelectAutocomplete";
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { FileUploader } from "@/components/ui/FileUploader";
import { VariantManager } from "@/components/ui/VariantManager";
import { ArrowRight, X } from "lucide-react";
@ -420,9 +419,19 @@ const ProductFormPage = () => {
if (isEdit && isLoadingProduct) {
return (
<div className="flex justify-center items-center h-64">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(8)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}

View File

@ -4,7 +4,7 @@ import { useProducts, useDeleteProduct } from '../core/_hooks';
import { useCategories } from '../../categories/core/_hooks';
import { Product } from '../core/_models';
import { Button } from "@/components/ui/Button";
import { PageContainer } from "@/components/ui/Typography";
import { Trash2, Edit3, Plus, Package, Eye, Image } from "lucide-react";
import { Modal } from "@/components/ui/Modal";
import { persianToEnglish } from '../../../utils/numberUtils';
@ -152,16 +152,17 @@ const ProductsListPage = () => {
if (error) {
return (
<div className="p-6">
<PageContainer>
<div className="text-center py-12">
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری محصولات</p>
</div>
</div>
</PageContainer>
);
}
return (
<div className="p-6 space-y-6">
<PageContainer>
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div>
@ -184,6 +185,23 @@ const ProductsListPage = () => {
{/* Filters */}
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg p-4">
{isLoading ? (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 animate-pulse">
{[...Array(4)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2"></div>
{i === 3 ? (
<div className="flex gap-2">
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg flex-1"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg flex-1"></div>
</div>
) : (
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
)}
</div>
))}
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
@ -194,7 +212,7 @@ const ProductsListPage = () => {
placeholder="جستجو در نام محصول..."
value={filters.search}
onChange={handleSearchChange}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
/>
</div>
<div>
@ -243,7 +261,7 @@ const ProductsListPage = () => {
const converted = persianToEnglish(e.target.value);
setFilters(prev => ({ ...prev, min_price: converted, page: 1 }));
}}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
/>
<input
type="text"
@ -254,11 +272,12 @@ const ProductsListPage = () => {
const converted = persianToEnglish(e.target.value);
setFilters(prev => ({ ...prev, max_price: converted, page: 1 }));
}}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
className="w-full px-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
/>
</div>
</div>
</div>
)}
</div>
{/* Products Table */}
@ -481,6 +500,7 @@ const ProductsListPage = () => {
</div>
</Modal>
</div>
</PageContainer>
);
};

View File

@ -1,7 +1,6 @@
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowRight, Shield, Users, Key, Edit, Calendar, FileText } from 'lucide-react';
import { Button } from '../../../components/ui/Button';
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
import { useRole } from '../core/_hooks';
import { PermissionWrapper } from '../../../components/common/PermissionWrapper';
import { PageContainer, PageTitle, SectionTitle, SectionSubtitle, BodyText } from '../../../components/ui/Typography';
@ -12,7 +11,50 @@ const RoleDetailPage = () => {
const { data: role, isLoading, error } = useRole(id);
if (isLoading) return <LoadingSpinner />;
if (isLoading) {
return (
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="flex items-center justify-between">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="flex gap-3">
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded w-32"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded w-24"></div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<div className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-6"></div>
<div className="space-y-6">
<div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
<div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2"></div>
<div className="h-20 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
</div>
</div>
</div>
<div className="space-y-6">
{[...Array(2)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
<div className="space-y-3">
{[...Array(2)].map((_, j) => (
<div key={j} className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</PageContainer>
);
}
if (error) return <div className="text-red-600">خطا در بارگذاری اطلاعات نقش</div>;
if (!role) return <div>نقش یافت نشد</div>;
@ -170,7 +212,7 @@ const RoleDetailPage = () => {
</div>
</div>
)}
</div>
</PageContainer>
);
};

View File

@ -7,7 +7,6 @@ import { useRole, useCreateRole, useUpdateRole } from '../core/_hooks';
import { RoleFormData } from '../core/_models';
import { Button } from "@/components/ui/Button";
import { Input } from "@/components/ui/Input";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { ArrowRight, Shield } from "lucide-react";
import { FormHeader, PageContainer, Label } from '../../../components/ui/Typography';
@ -70,7 +69,21 @@ const RoleFormPage = () => {
};
if (isEdit && roleLoading) {
return <LoadingSpinner />;
return (
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(3)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}
const isLoading = creating || updating;

View File

@ -2,10 +2,10 @@ import { useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useRole, useRolePermissions, usePermissions, useAssignPermission, useRemovePermission } from "../core/_hooks";
import { Button } from "@/components/ui/Button";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { Permission } from "@/types/auth";
import { ArrowRight, Plus, Trash2, Check, Shield } from "lucide-react";
import { Modal } from "@/components/ui/Modal";
import { PageContainer } from "@/components/ui/Typography";
const RolePermissionsPage = () => {
const navigate = useNavigate();
@ -56,11 +56,33 @@ const RolePermissionsPage = () => {
const isLoading = roleLoading || permissionsLoading;
if (isLoading) return <LoadingSpinner />;
if (isLoading) {
return (
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-64"></div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{[...Array(2)].map((_, i) => (
<div key={i} className="card">
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
</div>
<div className="p-6 space-y-3">
{[...Array(5)].map((_, j) => (
<div key={j} className="h-12 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
))}
</div>
</div>
</PageContainer>
);
}
if (!role) return <div className="text-red-600">نقش یافت نشد</div>;
return (
<div className="p-6">
<PageContainer>
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
@ -93,8 +115,10 @@ const RolePermissionsPage = () => {
<div className="p-6">
{permissionsLoading ? (
<div className="flex justify-center">
<LoadingSpinner />
<div className="space-y-3 animate-pulse">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-12 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
) : (
<div className="space-y-3">
@ -143,8 +167,10 @@ const RolePermissionsPage = () => {
<div className="p-6">
{allPermissionsLoading ? (
<div className="flex justify-center">
<LoadingSpinner />
<div className="space-y-3 animate-pulse">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-12 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
) : (
<div className="space-y-3">
@ -214,7 +240,7 @@ const RolePermissionsPage = () => {
</div>
</Modal>
</div>
</div>
</PageContainer>
);
};

View File

@ -4,7 +4,6 @@ import { useCreateShippingMethod, useShippingMethod, useUpdateShippingMethod } f
import { ShippingOpenHour } from '../core/_models';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { TagInput } from '@/components/ui/TagInput';
import { Truck } from 'lucide-react';
import { formatWithThousands, parseFormattedNumber } from '@/utils/numberUtils';
@ -107,9 +106,19 @@ const ShippingMethodFormPage = () => {
if (isEdit && isLoading) {
return (
<div className="min-h-[200px] flex items-center justify-center">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(5)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}

View File

@ -2,7 +2,7 @@ import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/Button';
import { Modal } from '@/components/ui/Modal';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { PageContainer } from '@/components/ui/Typography';
import { Plus, Edit3, Trash2, Truck } from 'lucide-react';
import { useShippingMethods, useDeleteShippingMethod } from '../core/_hooks';
import { ShippingMethod } from '../core/_models';
@ -28,24 +28,59 @@ const ShippingMethodsListPage = () => {
if (isLoading) {
return (
<div className="p-6 flex justify-center">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6">
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div>
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-64 mb-2 animate-pulse"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-48 animate-pulse"></div>
</div>
<div className="h-12 w-12 bg-gray-200 dark:bg-gray-700 rounded-full animate-pulse"></div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead className="bg-gray-50 dark:bg-gray-700">
<tr>
{[...Array(7)].map((_, i) => (
<th key={i} className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded w-20 animate-pulse"></div>
</th>
))}
</tr>
</thead>
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
{[...Array(5)].map((_, i) => (
<tr key={i} className="animate-pulse">
{[...Array(7)].map((_, j) => (
<td key={j} className="px-6 py-4 whitespace-nowrap">
<div className="h-4 bg-gray-200 dark:bg-gray-600 rounded"></div>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</PageContainer>
);
}
if (error) {
return (
<div className="p-6">
<PageContainer>
<div className="text-center py-12">
<p className="text-red-600 dark:text-red-400">خطا در بارگذاری روشهای ارسال</p>
</div>
</div>
</PageContainer>
);
}
return (
<div className="p-6 space-y-6">
<PageContainer>
<div className="space-y-6">
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100 flex items-center gap-2">
@ -143,6 +178,7 @@ const ShippingMethodsListPage = () => {
</div>
</Modal>
</div>
</PageContainer>
);
};

View File

@ -10,7 +10,6 @@ import {
import { TicketStatus } from "../core/_models";
import { PageContainer, PageTitle, SectionTitle, Label } from "@/components/ui/Typography";
import { Button } from "@/components/ui/Button";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { FileUploader } from "@/components/ui/FileUploader";
import { useFileUpload, useFileDelete } from "@/hooks/useFileUpload";
import { Input } from "@/components/ui/Input";
@ -155,9 +154,19 @@ const TicketDetailPage = () => {
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen">
<LoadingSpinner />
<PageContainer>
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(5)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);
}

View File

@ -132,7 +132,9 @@ const TicketsListPage = () => {
return (
<PageContainer>
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div>
<PageTitle className="flex items-center gap-2">
<MessageSquare className="h-6 w-6" />
@ -154,15 +156,63 @@ const TicketsListPage = () => {
</div>
</div>
<div className="card p-4 space-y-4">
{/* Filters */}
<div className="card p-4 sm:p-6">
{isLoading ? (
<div className="space-y-4 animate-pulse">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
</div>
<div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[...Array(3)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded-lg"></div>
</div>
))}
</div>
</div>
) : (
<>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<Filter className="h-5 w-5 text-gray-500" />
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">فیلترها</h3>
</div>
<Button
variant="secondary"
size="sm"
onClick={() =>
setFilters({
page: 1,
limit: 20,
search: "",
})
}
>
<Filter className="h-4 w-4 ml-2" />
پاک کردن فیلترها
</Button>
</div>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="relative">
<Search className="absolute right-3 top-3 h-4 w-4 text-gray-400" />
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
جستجو
</label>
<Search className="absolute right-3 top-10 h-4 w-4 text-gray-400" />
<input
value={filters.search || ""}
onChange={(e) => handleFilterChange("search", e.target.value)}
placeholder="جستجو در عنوان یا شماره تیکت"
className="w-full pr-9 pl-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-primary-500 dark:bg-gray-700 dark:text-gray-100"
className="w-full pr-9 pl-3 py-3 text-base border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-700 dark:text-gray-100 transition-all duration-200"
/>
</div>
<Input
@ -178,9 +228,9 @@ const TicketsListPage = () => {
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
وضعیت
</label>
<select
@ -202,7 +252,7 @@ const TicketsListPage = () => {
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
دپارتمان
</label>
<select
@ -223,25 +273,13 @@ const TicketsListPage = () => {
))}
</select>
</div>
<div className="flex items-end">
<Button
variant="secondary"
className="w-full flex items-center justify-center gap-2"
onClick={() =>
setFilters({
page: 1,
limit: 20,
search: "",
})
}
>
<Filter className="h-4 w-4" />
پاک کردن فیلترها
</Button>
</div>
</div>
</>
)}
</div>
{/* Table */}
{isLoading ? (
<Table columns={columns} data={[]} loading />
) : !data?.tickets || data.tickets.length === 0 ? (
@ -251,6 +289,7 @@ const TicketsListPage = () => {
) : (
<>
<Table columns={columns} data={data.tickets as any[]} />
{data.total > 0 && (
<Pagination
currentPage={filters.page || 1}
totalPages={Math.max(
@ -261,8 +300,10 @@ const TicketsListPage = () => {
itemsPerPage={filters.limit || 20}
totalItems={data.total || 0}
/>
)}
</>
)}
</div>
</PageContainer>
);
};

View File

@ -6,7 +6,6 @@ import { englishToPersian } from '../../../utils/numberUtils';
import { PageContainer } from '../../../components/ui/Typography';
import { Button } from '../../../components/ui/Button';
import { Modal } from '../../../components/ui/Modal';
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
const UserAdminDetailPage: React.FC = () => {
const { id } = useParams<{ id: string }>();
@ -56,8 +55,40 @@ const UserAdminDetailPage: React.FC = () => {
if (isLoading) {
return (
<PageContainer>
<div className="flex justify-center items-center py-12">
<LoadingSpinner />
<div className="space-y-6 animate-pulse">
<div className="flex items-center justify-between">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded w-24"></div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
{[...Array(3)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-4"></div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[...Array(4)].map((_, j) => (
<div key={j}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32"></div>
</div>
))}
</div>
</div>
))}
</div>
<div className="space-y-6">
{[...Array(2)].map((_, i) => (
<div key={i} className="card p-6">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
<div className="space-y-3">
{[...Array(3)].map((_, j) => (
<div key={j} className="h-4 bg-gray-200 dark:bg-gray-700 rounded"></div>
))}
</div>
</div>
))}
</div>
</div>
</div>
</PageContainer>
);

View File

@ -9,7 +9,6 @@ import { CreateUserRequest, UpdateUserRequest } from '../core/_models';
import { PageContainer } from '../../../components/ui/Typography';
import { Button } from '../../../components/ui/Button';
import { Input } from '../../../components/ui/Input';
import { LoadingSpinner } from '../../../components/ui/LoadingSpinner';
// Validation schema
const createUserSchema = yup.object({
@ -139,8 +138,16 @@ const UserAdminFormPage: React.FC = () => {
if (isEdit && userLoading) {
return (
<PageContainer>
<div className="flex justify-center items-center py-12">
<LoadingSpinner />
<div className="space-y-6 animate-pulse">
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48"></div>
<div className="card p-6 space-y-6">
{[...Array(6)].map((_, i) => (
<div key={i}>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-32 mb-2"></div>
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded"></div>
</div>
))}
</div>
</div>
</PageContainer>
);

View File

@ -1,7 +1,6 @@
import React from 'react';
import { Wallet, Loader2 } from 'lucide-react';
import { PageContainer, PageTitle } from '@/components/ui/Typography';
import { LoadingSpinner } from '@/components/ui/LoadingSpinner';
import { useWalletStatus, useUpdateWalletStatus } from '../core/_hooks';
import { WalletStatus, WALLET_LABELS } from '../core/_models';
@ -64,8 +63,32 @@ const WalletListPage = () => {
if (isLoading) {
return (
<PageContainer>
<div className="flex justify-center items-center h-64">
<LoadingSpinner />
<div className="flex flex-col space-y-3 sm:flex-row sm:items-center sm:justify-between sm:space-y-0">
<div>
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-48 mb-2 animate-pulse"></div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-64 animate-pulse"></div>
</div>
</div>
<div className="bg-white dark:bg-gray-800 shadow-sm border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<div className="p-6">
<div className="space-y-4">
{[...Array(3)].map((_, i) => (
<div
key={i}
className="flex items-center justify-between p-4 border border-gray-200 dark:border-gray-700 rounded-lg animate-pulse"
>
<div className="flex-1">
<div className="flex items-center gap-3">
<div className="h-6 bg-gray-200 dark:bg-gray-700 rounded w-32"></div>
<div className="h-5 bg-gray-200 dark:bg-gray-700 rounded w-16"></div>
</div>
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-48 mt-2"></div>
</div>
<div className="h-6 w-11 bg-gray-200 dark:bg-gray-700 rounded-full"></div>
</div>
))}
</div>
</div>
</div>
</PageContainer>
);