diff --git a/app/admin/dashboard/page.tsx b/app/admin/dashboard/page.tsx
index a74df3f..8231569 100644
--- a/app/admin/dashboard/page.tsx
+++ b/app/admin/dashboard/page.tsx
@@ -2,13 +2,11 @@
import { useAuth } from '@/contexts/AuthContext';
import Link from 'next/link';
-import { UsersIcon, CubeIcon, TagIcon , BellIcon, GiftIcon, PowerIcon, ShoppingBagIcon } from '@heroicons/react/24/outline';
+import { UsersIcon, CubeIcon, TagIcon, BellIcon, GiftIcon, ShoppingBagIcon } from '@heroicons/react/24/outline';
import { MegaphoneIcon } from 'lucide-react';
-import ThemeToggle from '@/components/ThemeToggle';
-// import { BellIcon } from 'lucide-react';
export default function Dashboard() {
- const { user, logout } = useAuth();
+ const { user } = useAuth();
const cards = [
{
@@ -16,93 +14,105 @@ export default function Dashboard() {
title: 'مدیریت کاربران',
desc: 'مشاهده و مدیریت کاربران، اشتراکها و تراکنشها',
Icon: UsersIcon,
- iconWrap: 'bg-indigo-100 text-indigo-600 dark:bg-indigo-500/15 dark:text-indigo-400',
+ iconBg: 'bg-indigo-100 dark:bg-indigo-500/15',
+ iconColor: 'text-indigo-600 dark:text-indigo-400',
+ hoverBorder: 'hover:border-indigo-300 dark:hover:border-indigo-700',
},
{
href: '/admin/purchases',
title: 'خریدهای اشتراک',
desc: 'مشاهده و فیلتر خریدها بر اساس ایمیل، موبایل، پکیج و منبع پرداخت',
Icon: ShoppingBagIcon,
- iconWrap: 'bg-orange-100 text-orange-600 dark:bg-orange-500/15 dark:text-orange-400',
+ iconBg: 'bg-orange-100 dark:bg-orange-500/15',
+ iconColor: 'text-orange-600 dark:text-orange-400',
+ hoverBorder: 'hover:border-orange-300 dark:hover:border-orange-700',
},
{
href: '/admin/packages',
title: 'مدیریت پکیجها',
desc: 'ایجاد و مدیریت پکیجهای نرمافزار',
Icon: CubeIcon,
- iconWrap: 'bg-green-100 text-green-600 dark:bg-green-500/15 dark:text-green-400',
+ iconBg: 'bg-green-100 dark:bg-green-500/15',
+ iconColor: 'text-green-600 dark:text-green-400',
+ hoverBorder: 'hover:border-green-300 dark:hover:border-green-700',
},
{
href: '/admin/products',
title: 'مدیریت محصولات',
desc: 'ایجاد و مدیریت محصولات و اشتراکها',
Icon: TagIcon,
- iconWrap: 'bg-purple-100 text-purple-600 dark:bg-purple-500/15 dark:text-purple-400',
+ iconBg: 'bg-purple-100 dark:bg-purple-500/15',
+ iconColor: 'text-purple-600 dark:text-purple-400',
+ hoverBorder: 'hover:border-purple-300 dark:hover:border-purple-700',
},
{
href: '/admin/reminders',
title: 'مدیریت یادآوریها',
desc: 'ایجاد و مدیریت یادآوریها و پیامهای انگیزشی',
Icon: BellIcon,
- iconWrap: 'bg-yellow-100 text-yellow-600 dark:bg-yellow-500/15 dark:text-yellow-400',
+ iconBg: 'bg-yellow-100 dark:bg-yellow-500/15',
+ iconColor: 'text-yellow-600 dark:text-yellow-400',
+ hoverBorder: 'hover:border-yellow-300 dark:hover:border-yellow-700',
},
{
href: '/admin/promotions',
title: 'مدیریت تبلیغات',
desc: 'ایجاد و مدیریت تبلیغات، بنرها و اسلایدرها',
Icon: MegaphoneIcon,
- iconWrap: 'bg-pink-100 text-pink-600 dark:bg-pink-500/15 dark:text-pink-400',
+ iconBg: 'bg-pink-100 dark:bg-pink-500/15',
+ iconColor: 'text-pink-600 dark:text-pink-400',
+ hoverBorder: 'hover:border-pink-300 dark:hover:border-pink-700',
},
{
href: '/admin/referral-rewards',
title: 'پاداشهای معرفی',
desc: 'اعطای اشتراک رایگان به کاربران واجد شرایط دعوت',
Icon: GiftIcon,
- iconWrap: 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/15 dark:text-emerald-400',
+ iconBg: 'bg-emerald-100 dark:bg-emerald-500/15',
+ iconColor: 'text-emerald-600 dark:text-emerald-400',
+ hoverBorder: 'hover:border-emerald-300 dark:hover:border-emerald-700',
},
];
return (
-
-
-
- داشبورد
+ {/* Welcome Section */}
+
+
+
+ خوش آمدید
-
+
+ {user?.email}
+
+
+ یکی از بخشهای زیر را برای مدیریت انتخاب کنید
+
+
-
-
خوش آمدید، {user?.email}
-
یکی از بخشهای زیر را برای مدیریت انتخاب کنید.
-
- {/* Dashboard Cards */}
-
- {cards.map(({ href, title, desc, Icon, iconWrap }) => (
-
-
-
-
-
-
{title}
-
- {desc}
-
-
-
- ))}
-
-
+ {/* Dashboard Cards */}
+
+ {cards.map(({ href, title, desc, Icon, iconBg, iconColor, hoverBorder }) => (
+
+
+
+
+
+
{title}
+
+ {desc}
+
+
+
+
+ ))}
diff --git a/app/admin/layout.tsx b/app/admin/layout.tsx
index e3b96e1..3492bbe 100644
--- a/app/admin/layout.tsx
+++ b/app/admin/layout.tsx
@@ -1,19 +1,34 @@
'use client';
import { AuthProvider } from '@/contexts/AuthContext';
-import ThemeToggle from '@/components/ThemeToggle';
+import AuthGuard from '@/components/AuthGuard';
+import Sidebar from '@/components/admin/Sidebar';
+import ToastContainer from '@/components/Toast';
+import { usePathname } from 'next/navigation';
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
+ const pathname = usePathname();
+ const isLogin = pathname === '/admin/login' || pathname === '/admin/login/';
+
return (
-
- {children}
-
+
+
+ {isLogin ? (
+ children
+ ) : (
+
+
+
+ {children}
+
+
+ )}
);
-}
\ No newline at end of file
+}
diff --git a/app/admin/login/page.tsx b/app/admin/login/page.tsx
index e047b4a..2c36b5d 100644
--- a/app/admin/login/page.tsx
+++ b/app/admin/login/page.tsx
@@ -1,9 +1,8 @@
'use client';
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { useAuth } from '@/contexts/AuthContext';
import { useRouter } from 'next/navigation';
-import { useEffect } from 'react';
import { LockClosedIcon } from '@heroicons/react/24/outline';
export default function LoginPage() {
@@ -25,7 +24,7 @@ export default function LoginPage() {
setLocalError('');
if (!auth || !password) {
- setLocalError('Please fill in all fields');
+ setLocalError('لطفاً همه فیلدها را پر کنید');
return;
}
@@ -37,33 +36,39 @@ export default function LoginPage() {
};
return (
-
-
-
+
+ {/* Decorative background */}
+
+
+
+
-
-
+
+
- پنل ادمین اپروایجنسی
+ پنل ادمین
- برای ادامه وارد حساب کاربری خود شوید
+ اپروایجنسی
);
-}
\ No newline at end of file
+}
diff --git a/app/admin/packages/page.tsx b/app/admin/packages/page.tsx
index 118c5ea..daa61b8 100644
--- a/app/admin/packages/page.tsx
+++ b/app/admin/packages/page.tsx
@@ -5,6 +5,8 @@ import { useAuth } from '@/contexts/AuthContext';
import { packagesApi } from '@/lib/api/packages';
import PackageList from '@/components/admin/packages/PackageList';
import PackageForm from '@/components/admin/packages/PackageForm';
+import PageHeader from '@/components/admin/PageHeader';
+import { showToast } from '@/components/Toast';
import { PackageName, CreatePackageData, UpdatePackageData } from '@/types/package';
import { PlusIcon } from '@heroicons/react/24/outline';
@@ -15,9 +17,7 @@ export default function PackagesPage() {
const [isLoading, setIsLoading] = useState(false);
const [isFormVisible, setIsFormVisible] = useState(false);
const [error, setError] = useState
(null);
- const [success, setSuccess] = useState(null);
- // Load packages on mount
useEffect(() => {
if (token) {
loadPackages();
@@ -54,11 +54,10 @@ export default function PackagesPage() {
setIsLoading(true);
setError(null);
- setSuccess(null);
try {
await packagesApi.deletePackage(pkg.name!, token!);
- setSuccess('پکیج با موفقیت حذف شد');
+ showToast('success', 'پکیج با موفقیت حذف شد');
await loadPackages();
} catch (err) {
setError(err instanceof Error ? err.message : 'خطا در حذف پکیج');
@@ -70,17 +69,14 @@ export default function PackagesPage() {
const handleSubmit = async (data: CreatePackageData | UpdatePackageData) => {
setIsLoading(true);
setError(null);
- setSuccess(null);
try {
if (selectedPackage) {
- // Update existing package
await packagesApi.updatePackage(selectedPackage.name!, data, token!);
- setSuccess('پکیج با موفقیت بهروزرسانی شد');
+ showToast('success', 'پکیج با موفقیت بهروزرسانی شد');
} else {
- // Create new package
await packagesApi.createPackage(data as CreatePackageData, token!);
- setSuccess('پکیج با موفقیت ایجاد شد');
+ showToast('success', 'پکیج با موفقیت ایجاد شد');
}
await loadPackages();
@@ -97,41 +93,37 @@ export default function PackagesPage() {
setIsFormVisible(false);
setSelectedPackage(null);
setError(null);
- setSuccess(null);
};
return (
-
+
-
-
- مدیریت پکیجها
-
- {!isFormVisible && (
-
- )}
-
+
+
+ پکیج جدید
+
+ ) : undefined
+ }
+ />
- {/* Messages */}
{error && (
-
-
{error}
+
+
{error}
+
)}
- {success && (
-
- )}
-
- {/* Form or List */}
{isFormVisible ? (
);
-}
\ No newline at end of file
+}
diff --git a/app/admin/products/page.tsx b/app/admin/products/page.tsx
index edfd078..65925d9 100644
--- a/app/admin/products/page.tsx
+++ b/app/admin/products/page.tsx
@@ -6,6 +6,8 @@ import { packagesApi } from '@/lib/api/packages';
import { productsApi } from '@/lib/api/products';
import ProductList from '@/components/admin/products/ProductList';
import ProductForm from '@/components/admin/products/ProductForm';
+import PageHeader from '@/components/admin/PageHeader';
+import { showToast } from '@/components/Toast';
import { PackageName } from '@/types/package';
import { Product, CreateProductData, UpdateProductData } from '@/types/product';
import { PlusIcon } from '@heroicons/react/24/outline';
@@ -20,13 +22,10 @@ export default function ProductsPage() {
const [isLoadingPackages, setIsLoadingPackages] = useState(false);
const [isFormVisible, setIsFormVisible] = useState(false);
const [error, setError] = useState(null);
- const [success, setSuccess] = useState(null);
- // Use refs to track initial loads and prevent duplicate calls
const initialLoadRef = useRef(false);
const prevPackageRef = useRef(null);
- // Load packages on mount - only once
useEffect(() => {
if (token && !initialLoadRef.current) {
initialLoadRef.current = true;
@@ -51,9 +50,7 @@ export default function ProductsPage() {
};
const loadProducts = useCallback(async (packageName: string, force = false) => {
- // Prevent duplicate loads of the same package
if (!force && prevPackageRef.current === packageName) {
- console.log('Skipping duplicate load for package:', packageName);
return;
}
@@ -62,7 +59,6 @@ export default function ProductsPage() {
prevPackageRef.current = packageName;
try {
- console.log('Loading products for package:', packageName);
const data = await productsApi.getProducts(packageName, token!);
setProducts(data);
} catch (err) {
@@ -72,7 +68,6 @@ export default function ProductsPage() {
}
}, [token]);
- // Load products when package changes - with proper dependency
useEffect(() => {
if (selectedPackage?.name && token) {
loadProducts(selectedPackage.name);
@@ -87,7 +82,6 @@ export default function ProductsPage() {
setIsFormVisible(false);
setSelectedProduct(null);
setError(null);
- setSuccess(null);
};
const handleCreate = () => {
@@ -102,21 +96,18 @@ export default function ProductsPage() {
const handleDelete = async (product: Product) => {
if (!selectedPackage) return;
-
if (!confirm(`آیا از حذف محصول "${product.title}" اطمینان دارید؟`)) {
return;
}
setIsLoading(true);
setError(null);
- setSuccess(null);
try {
await productsApi.deleteProduct(selectedPackage.name!, product.id, token!);
- setSuccess('محصول با موفقیت حذف شد');
- // Force reload products after delete
+ showToast('success', 'محصول با موفقیت حذف شد');
if (selectedPackage.name) {
- prevPackageRef.current = null; // Reset ref to force load
+ prevPackageRef.current = null;
await loadProducts(selectedPackage.name, true);
}
} catch (err) {
@@ -131,22 +122,18 @@ export default function ProductsPage() {
setIsLoading(true);
setError(null);
- setSuccess(null);
try {
if (selectedProduct) {
- // Update existing product
await productsApi.updateProduct(selectedPackage.name!, selectedProduct.id, data, token!);
- setSuccess('محصول با موفقیت بهروزرسانی شد');
+ showToast('success', 'محصول با موفقیت بهروزرسانی شد');
} else {
- // Create new product
await productsApi.createProduct(selectedPackage.name!, data as CreateProductData, token!);
- setSuccess('محصول با موفقیت ایجاد شد');
+ showToast('success', 'محصول با موفقیت ایجاد شد');
}
- // Force reload products after create/update
if (selectedPackage.name) {
- prevPackageRef.current = null; // Reset ref to force load
+ prevPackageRef.current = null;
await loadProducts(selectedPackage.name, true);
}
@@ -163,20 +150,15 @@ export default function ProductsPage() {
setIsFormVisible(false);
setSelectedProduct(null);
setError(null);
- setSuccess(null);
};
return (
-
+
-
-
- مدیریت محصولات
-
-
+
{/* Package Selector */}
-
+
@@ -185,7 +167,7 @@ export default function ProductsPage() {
value={selectedPackage?.name || ''}
onChange={handlePackageChange}
disabled={isLoadingPackages}
- className="block w-full px-3 py-2 text-slate-900 dark:text-gray-100 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:opacity-50 dark:bg-gray-800 dark:placeholder-gray-500 transition-colors"
+ className="block w-full px-3 py-2.5 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent sm:text-sm disabled:opacity-50 dark:bg-gray-800/80 transition-colors"
>
{isLoadingPackages ? (
@@ -199,20 +181,17 @@ export default function ProductsPage() {
- {/* Messages */}
{error && (
-
-
{error}
+
+
{error}
+
)}
- {success && (
-
- )}
-
- {/* Form or List */}
{selectedPackage && (
<>
{!isFormVisible && (
@@ -220,9 +199,9 @@ export default function ProductsPage() {
@@ -250,4 +229,4 @@ export default function ProductsPage() {
);
-}
\ No newline at end of file
+}
diff --git a/app/admin/promotions/page.tsx b/app/admin/promotions/page.tsx
index 07a4c12..767012e 100644
--- a/app/admin/promotions/page.tsx
+++ b/app/admin/promotions/page.tsx
@@ -5,6 +5,8 @@ import { useAuth } from '@/contexts/AuthContext';
import { promotionsApi } from '@/lib/api/promotions';
import PromotionList from '@/components/admin/promotions/PromotionList';
import PromotionForm from '@/components/admin/promotions/PromotionForm';
+import PageHeader from '@/components/admin/PageHeader';
+import { showToast } from '@/components/Toast';
import { Promotion, CreatePromotionData, UpdatePromotionData } from '@/types/promotion';
import { PlusIcon } from '@heroicons/react/24/outline';
@@ -15,11 +17,9 @@ export default function PromotionsPage() {
const [isLoading, setIsLoading] = useState(false);
const [isFormVisible, setIsFormVisible] = useState(false);
const [error, setError] = useState
(null);
- const [success, setSuccess] = useState(null);
-
+
const initialLoadRef = useRef(false);
- // Load promotions on mount
useEffect(() => {
if (token && !initialLoadRef.current) {
initialLoadRef.current = true;
@@ -57,11 +57,10 @@ export default function PromotionsPage() {
setIsLoading(true);
setError(null);
- setSuccess(null);
try {
await promotionsApi.deletePromotion(promotion.id, token!);
- setSuccess('تبلیغ با موفقیت حذف شد');
+ showToast('success', 'تبلیغ با موفقیت حذف شد');
await loadPromotions();
} catch (err) {
setError(err instanceof Error ? err.message : 'خطا در حذف تبلیغ');
@@ -73,11 +72,10 @@ export default function PromotionsPage() {
const handleToggleActive = async (promotion: Promotion) => {
setIsLoading(true);
setError(null);
- setSuccess(null);
try {
await promotionsApi.toggleActive(promotion.id, !promotion.is_active, token!);
- setSuccess(`تبلیغ با موفقیت ${!promotion.is_active ? 'فعال' : 'غیرفعال'} شد`);
+ showToast('success', `تبلیغ با موفقیت ${!promotion.is_active ? 'فعال' : 'غیرفعال'} شد`);
await loadPromotions();
} catch (err) {
setError(err instanceof Error ? err.message : 'خطا در تغییر وضعیت تبلیغ');
@@ -89,17 +87,14 @@ export default function PromotionsPage() {
const handleSubmit = async (data: CreatePromotionData | UpdatePromotionData) => {
setIsLoading(true);
setError(null);
- setSuccess(null);
try {
if (selectedPromotion) {
- // Update existing promotion
await promotionsApi.updatePromotion(selectedPromotion.id, data, token!);
- setSuccess('تبلیغ با موفقیت بهروزرسانی شد');
+ showToast('success', 'تبلیغ با موفقیت بهروزرسانی شد');
} else {
- // Create new promotion
await promotionsApi.createPromotion(data as CreatePromotionData, token!);
- setSuccess('تبلیغ با موفقیت ایجاد شد');
+ showToast('success', 'تبلیغ با موفقیت ایجاد شد');
}
await loadPromotions();
@@ -116,42 +111,38 @@ export default function PromotionsPage() {
setIsFormVisible(false);
setSelectedPromotion(null);
setError(null);
- setSuccess(null);
};
return (
-
+
-
-
- مدیریت تبلیغات
-
- {!isFormVisible && (
-
- )}
-
+
+
+ تبلیغ جدید
+
+ ) : undefined
+ }
+ />
- {/* Messages */}
{error && (
-
-
{error}
+
+
{error}
+
)}
- {success && (
-
- )}
-
- {/* Form or List */}
{isFormVisible ? (
);
-}
\ No newline at end of file
+}
diff --git a/app/admin/purchases/page.tsx b/app/admin/purchases/page.tsx
index 9eb05a6..fc773a1 100644
--- a/app/admin/purchases/page.tsx
+++ b/app/admin/purchases/page.tsx
@@ -1,10 +1,10 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
-import Link from 'next/link';
import { useAuth } from '@/contexts/AuthContext';
import { purchasesApi } from '@/lib/api/purchases';
import { packagesApi } from '@/lib/api/packages';
+import PageHeader from '@/components/admin/PageHeader';
import {
Purchase,
Paginated,
@@ -16,15 +16,15 @@ import {
} from '@/types/purchase';
import { PackageName } from '@/types/package';
import { formatPrice, formatDate, getProductTypeLabel } from '@/lib/utils';
-import { ArrowRightIcon, MagnifyingGlassIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
+import { MagnifyingGlassIcon, ArrowPathIcon } from '@heroicons/react/24/outline';
const statusBadgeClass = (status: number): string => {
switch (status) {
- case 2: // موفق
+ case 2:
return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300';
- case 3: // مصرفشده
+ case 3:
return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300';
- default: // در انتظار
+ default:
return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300';
}
};
@@ -63,7 +63,6 @@ export default function PurchasesPage() {
}
}, [token]);
- // Load package list (for the filter dropdown) and the first page
useEffect(() => {
if (!token) return;
packagesApi.getAllPackages(token).then(setPackages).catch(() => setPackages([]));
@@ -96,31 +95,20 @@ export default function PurchasesPage() {
};
const inputClass =
- 'block w-full px-3 py-2 text-slate-900 dark:text-gray-100 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm dark:bg-gray-800 dark:placeholder-gray-500 transition-colors';
- const labelClass = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1';
+ 'block w-full px-3 py-2.5 text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 rounded-xl shadow-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent sm:text-sm dark:bg-gray-800/80 transition-colors';
+ const labelClass = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5';
return (
-
+
-
-
- خریدهای اشتراک
-
-
-
- بازگشت به داشبورد
-
-
+
{/* Filters */}