From a447222201b6a45a109cce2b1842ced089ced454 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Fri, 10 Jul 2026 16:01:27 +0330 Subject: [PATCH] fix: redesign --- app/admin/dashboard/page.tsx | 102 +++--- app/admin/layout.tsx | 25 +- app/admin/login/page.tsx | 54 +-- app/admin/packages/page.tsx | 64 ++-- app/admin/products/page.tsx | 63 ++-- app/admin/promotions/page.tsx | 71 ++-- app/admin/purchases/page.tsx | 92 +++--- app/admin/referral-rewards/page.tsx | 62 ++-- app/admin/reminders/page.tsx | 61 ++-- app/admin/users/page.tsx | 183 ++++------- app/globals.css | 39 ++- components/AuthGuard.tsx | 33 ++ components/Toast.tsx | 69 ++++ components/admin/PageHeader.tsx | 25 ++ components/admin/Sidebar.tsx | 132 ++++++++ components/admin/StepIndicator.tsx | 61 ++++ components/admin/packages/PackageForm.tsx | 215 +++--------- components/admin/packages/PackageList.tsx | 71 ++-- components/admin/products/ProductForm.tsx | 209 +++--------- components/admin/products/ProductList.tsx | 112 +++---- components/admin/promotions/PromotionForm.tsx | 307 ++++-------------- components/admin/promotions/PromotionList.tsx | 215 +++++------- components/admin/referral/ReferralList.tsx | 77 +++-- components/admin/reminders/ReminderForm.tsx | 175 ++-------- components/admin/reminders/ReminderList.tsx | 87 +++-- components/admin/users/PackageSelector.tsx | 91 ++++-- components/admin/users/UserEditForm.tsx | 81 ++--- components/admin/users/UserInfo.tsx | 155 +++++---- components/admin/users/UserProducts.tsx | 151 ++++----- components/admin/users/UserSearch.tsx | 52 +-- middleware.ts | 30 -- 31 files changed, 1426 insertions(+), 1738 deletions(-) create mode 100644 components/AuthGuard.tsx create mode 100644 components/Toast.tsx create mode 100644 components/admin/PageHeader.tsx create mode 100644 components/admin/Sidebar.tsx create mode 100644 components/admin/StepIndicator.tsx delete mode 100644 middleware.ts 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 */} +
+
+
+
+ +
+
-
- +
+

- پنل ادمین اپروایجنسی + پنل ادمین

- برای ادامه وارد حساب کاربری خود شوید + اپروایجنسی

-
-
{(error || localError) && ( -
-

+
+

{localError || error} -

+

)} @@ -102,9 +107,16 @@ export default function LoginPage() {
@@ -112,4 +124,4 @@ export default function LoginPage() {
); -} \ 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 && ( -
-

{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 && ( -
-

{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 && ( -
-

{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 */}
-
+
@@ -160,36 +148,41 @@ export default function PurchasesPage() {
-
+
{error && ( -
-

{error}

+
+

{error}

+
)} {/* Results */} -
-
+
+

{result ? `${formatPrice(result.total)} خرید یافت شد` : 'در حال بارگذاری...'}

@@ -197,11 +190,16 @@ export default function PurchasesPage() { {isLoading ? (
-
+
) : !result || result.data.length === 0 ? (
-

هیچ خریدی با این فیلترها یافت نشد

+
+ + + +
+

هیچ خریدی با این فیلترها یافت نشد

) : (
@@ -219,8 +217,8 @@ export default function PurchasesPage() { {result.data.map((purchase) => ( - - + +
{purchase.user?.full_name?.trim() || 'بدون نام'}
@@ -231,10 +229,10 @@ export default function PurchasesPage() { {purchase.user?.mobile || '—'}
- + {purchase.product?.package_name?.title || purchase.product?.package_name?.name || '—'} - +
{purchase.product?.title || '—'}
@@ -242,20 +240,20 @@ export default function PurchasesPage() { {purchase.product ? getProductTypeLabel(purchase.product.type) : ''}
- + {formatPrice(purchase.amount)} تومان - - + + {getGatewayLabel(purchase.gateway)} - - + + {getPurchaseStatusLabel(purchase.status)} - + {formatDate(purchase.created_at)} @@ -267,11 +265,11 @@ export default function PurchasesPage() { {/* Pagination */} {result && result.last_page > 1 && ( -
+
@@ -281,7 +279,7 @@ export default function PurchasesPage() { diff --git a/app/admin/referral-rewards/page.tsx b/app/admin/referral-rewards/page.tsx index 5b00236..b4e0ed8 100644 --- a/app/admin/referral-rewards/page.tsx +++ b/app/admin/referral-rewards/page.tsx @@ -4,6 +4,8 @@ import { useState, useEffect, useRef } from 'react'; import { useAuth } from '@/contexts/AuthContext'; import { referralApi } from '@/lib/api/referral'; import ReferralList from '@/components/admin/referral/ReferralList'; +import PageHeader from '@/components/admin/PageHeader'; +import { showToast } from '@/components/Toast'; import { ReferralRewardUser } from '@/types/referral'; import { ArrowPathIcon } from '@heroicons/react/24/outline'; @@ -14,7 +16,6 @@ export default function ReferralRewardsPage() { const [isLoading, setIsLoading] = useState(false); const [fulfillingId, setFulfillingId] = useState(null); const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); const initialLoadRef = useRef(false); @@ -47,12 +48,10 @@ export default function ReferralRewardsPage() { setFulfillingId(user.id); setIsLoading(true); setError(null); - setSuccess(null); try { await referralApi.fulfillReward(user.id, token!); - setSuccess(`اشتراک رایگان با موفقیت به «${user.name || user.mobile || user.email}» اعطا شد`); - // Remove the fulfilled user from the list and refresh + showToast('success', `اشتراک رایگان با موفقیت به «${user.name || user.mobile || user.email}» اعطا شد`); setUsers((prev) => prev.filter((u) => u.id !== user.id)); } catch (err) { setError(err instanceof Error ? err.message : 'خطا در اعطای اشتراک رایگان'); @@ -63,43 +62,38 @@ export default function ReferralRewardsPage() { }; return ( -
+
-
-
-

- پاداش‌های معرفی -

-

- کاربرانی که به حد نصاب دعوت موفق رسیده‌اند و هنوز اشتراک رایگان دریافت نکرده‌اند. -

-
- -
+ + + بروزرسانی + + } + /> - {/* Messages */} {error && ( -
-

{error}

-
- )} - - {success && ( -
-

{success}

+
+

{error}

+
)} {isLoading && users.length === 0 ? ( -
-

در حال بارگذاری...

+
+
+

در حال بارگذاری...

) : ( (null); - const [success, setSuccess] = useState(null); - + const initialLoadRef = useRef(false); const prevPackageRef = useRef(null); - // Load packages on mount useEffect(() => { if (token && !initialLoadRef.current) { initialLoadRef.current = true; @@ -53,11 +53,11 @@ export default function RemindersPage() { if (!force && prevPackageRef.current === packageName) { return; } - + setIsLoading(true); setError(null); prevPackageRef.current = packageName; - + try { const data = await remindersApi.getReminders(packageName, token!); setReminders(data); @@ -68,7 +68,6 @@ export default function RemindersPage() { } }, [token]); - // Load reminders when package changes useEffect(() => { if (selectedPackage?.name && token) { loadReminders(selectedPackage.name); @@ -83,7 +82,6 @@ export default function RemindersPage() { setIsFormVisible(false); setSelectedReminder(null); setError(null); - setSuccess(null); }; const handleCreate = () => { @@ -98,18 +96,16 @@ export default function RemindersPage() { const handleDelete = async (reminder: Reminder) => { if (!selectedPackage) return; - if (!confirm(`آیا از حذف یادآوری "${reminder.title}" اطمینان دارید؟`)) { return; } setIsLoading(true); setError(null); - setSuccess(null); try { await remindersApi.deleteReminder(selectedPackage.name!, reminder.id, token!); - setSuccess('یادآوری با موفقیت حذف شد'); + showToast('success', 'یادآوری با موفقیت حذف شد'); if (selectedPackage.name) { prevPackageRef.current = null; await loadReminders(selectedPackage.name, true); @@ -126,24 +122,21 @@ export default function RemindersPage() { setIsLoading(true); setError(null); - setSuccess(null); try { if (selectedReminder) { - // Update existing reminder await remindersApi.updateReminder(selectedPackage.name!, selectedReminder.id, data, token!); - setSuccess('یادآوری با موفقیت به‌روزرسانی شد'); + showToast('success', 'یادآوری با موفقیت به‌روزرسانی شد'); } else { - // Create new reminder await remindersApi.createReminder(selectedPackage.name!, data as CreateReminderData, token!); - setSuccess('یادآوری با موفقیت ایجاد شد'); + showToast('success', 'یادآوری با موفقیت ایجاد شد'); } if (selectedPackage.name) { prevPackageRef.current = null; await loadReminders(selectedPackage.name, true); } - + setIsFormVisible(false); setSelectedReminder(null); } catch (err) { @@ -157,20 +150,15 @@ export default function RemindersPage() { setIsFormVisible(false); setSelectedReminder(null); setError(null); - setSuccess(null); }; return ( -
+
-
-

- مدیریت یادآوری‌ها -

-
+ {/* Package Selector */} -
+
@@ -179,7 +167,7 @@ export default function RemindersPage() { 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 dark:bg-gray-800 dark:placeholder-gray-500 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm disabled:opacity-50 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 ? ( @@ -193,20 +181,17 @@ export default function RemindersPage() {
- {/* Messages */} {error && ( -
-

{error}

+
+

{error}

+
)} - {success && ( -
-

{success}

-
- )} - - {/* Form or List */} {selectedPackage && ( <> {!isFormVisible && ( @@ -214,9 +199,9 @@ export default function RemindersPage() {
@@ -244,4 +229,4 @@ export default function RemindersPage() {
); -} \ No newline at end of file +} diff --git a/app/admin/users/page.tsx b/app/admin/users/page.tsx index 5be74d2..a0bef82 100644 --- a/app/admin/users/page.tsx +++ b/app/admin/users/page.tsx @@ -9,6 +9,9 @@ import UserInfo from '@/components/admin/users/UserInfo'; import UserProducts from '@/components/admin/users/UserProducts'; import UserEditForm from '@/components/admin/users/UserEditForm'; import PackageSelector from '@/components/admin/users/PackageSelector'; +import PageHeader from '@/components/admin/PageHeader'; +import StepIndicator from '@/components/admin/StepIndicator'; +import { showToast } from '@/components/Toast'; import { User, Product, UpdateUserProfileData } from '@/types/user'; import { PackageName } from '@/types/package'; import { formatPrice, getProductTypeLabel } from '@/lib/utils'; @@ -22,17 +25,14 @@ export default function UsersPage() { const [selectedPackage, setSelectedPackage] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const [success, setSuccess] = useState(null); const [isEditing, setIsEditing] = useState(false); - // Load packages on mount useEffect(() => { if (token) { loadPackages(); } }, [token]); - // Load products when package changes useEffect(() => { if (token && selectedPackage) { loadProducts(selectedPackage); @@ -54,9 +54,7 @@ export default function UsersPage() { const loadProducts = async (packageName: string) => { try { - console.log('Loading products for package:', packageName); const data = await usersApi.getProductsByPackage(packageName, token!); - console.log('Products loaded:', data); setProducts(data); } catch (err) { console.error('Failed to load products:', err); @@ -75,9 +73,7 @@ export default function UsersPage() { setUser(null); try { - console.log('Searching for user:', searchTerm, 'in package:', selectedPackage); const userData = await usersApi.getUserStatus(searchTerm, selectedPackage, token!); - console.log('User data:', userData); setUser(userData); } catch (err) { setError(err instanceof Error ? err.message : 'خطا در دریافت اطلاعات کاربر'); @@ -88,10 +84,7 @@ export default function UsersPage() { const handleSubscribe = async (productId: number) => { if (!user) return; - - // Get identifier (email or mobile) with null check const identifier = user.email || user.mobile; - if (!identifier) { setError('کاربر فاقد ایمیل یا شماره موبایل است'); return; @@ -99,13 +92,10 @@ export default function UsersPage() { setIsLoading(true); setError(null); - setSuccess(null); try { - console.log('Subscribing user:', identifier, 'to product:', productId); await usersApi.subscribeUser(identifier, productId, token!); - setSuccess('اشتراک با موفقیت اضافه شد'); - // Refresh user data with current selected package + showToast('success', 'اشتراک با موفقیت اضافه شد'); if (selectedPackage) { const userData = await usersApi.getUserStatus(identifier, selectedPackage, token!); setUser(userData); @@ -119,10 +109,7 @@ export default function UsersPage() { const handleUnsubscribe = async (productId: number) => { if (!user) return; - - // Get identifier (email or mobile) with null check const identifier = user.email || user.mobile; - if (!identifier) { setError('کاربر فاقد ایمیل یا شماره موبایل است'); return; @@ -130,13 +117,10 @@ export default function UsersPage() { setIsLoading(true); setError(null); - setSuccess(null); try { - console.log('Unsubscribing user:', identifier, 'from product:', productId); await usersApi.unsubscribeUser(identifier, productId, token!); - setSuccess('اشتراک با موفقیت لغو شد'); - // Refresh user data with current selected package + showToast('success', 'اشتراک با موفقیت لغو شد'); if (selectedPackage) { const userData = await usersApi.getUserStatus(identifier, selectedPackage, token!); setUser(userData); @@ -150,9 +134,7 @@ export default function UsersPage() { const handleEditSubmit = async (data: UpdateUserProfileData) => { if (!user) return; - const identifier = user.email || user.mobile; - if (!identifier) { setError('کاربر فاقد ایمیل یا شماره موبایل است'); return; @@ -160,13 +142,11 @@ export default function UsersPage() { setIsLoading(true); setError(null); - setSuccess(null); try { await usersApi.updateUserProfile(identifier, data, token!); - setSuccess('اطلاعات کاربر با موفقیت به‌روزرسانی شد'); + showToast('success', 'اطلاعات کاربر با موفقیت به‌روزرسانی شد'); setIsEditing(false); - // Refresh user data using the (possibly) new identifier const newIdentifier = data.email || data.mobile || identifier; if (selectedPackage) { const userData = await usersApi.getUserStatus(newIdentifier, selectedPackage, token!); @@ -181,9 +161,7 @@ export default function UsersPage() { const handleDeleteUser = async () => { if (!user) return; - const identifier = user.email || user.mobile; - if (!identifier) { setError('کاربر فاقد ایمیل یا شماره موبایل است'); return; @@ -195,11 +173,10 @@ export default function UsersPage() { setIsLoading(true); setError(null); - setSuccess(null); try { await usersApi.deleteUser(identifier, token!); - setSuccess('کاربر با موفقیت حذف شد'); + showToast('success', 'کاربر با موفقیت حذف شد'); setUser(null); } catch (err) { setError(err instanceof Error ? err.message : 'خطا در حذف کاربر'); @@ -209,32 +186,30 @@ export default function UsersPage() { }; const handlePackageChange = (packageName: string) => { - console.log('Package changed to:', packageName); setSelectedPackage(packageName); - // Clear user when package changes to avoid showing wrong data setUser(null); setError(null); - setSuccess(null); }; - // Filter available products (show ALL products, don't filter out ones user already has) const availableProducts = products; - // console.log('Current state:', { - // selectedPackage, - // productsCount: products.length, - // userProductsCount: user?.products?.length, - // availableProductsCount: availableProducts.length - // }); + // Compute step indicator state + const steps = [ + { label: 'انتخاب پکیج', isCompleted: !!selectedPackage, isCurrent: !selectedPackage }, + { label: 'جستجوی کاربر', isCompleted: !!user, isCurrent: !!selectedPackage && !user }, + { label: 'پروفایل کاربر', isCompleted: false, isCurrent: !!user }, + ]; return ( -
+
-

- مدیریت کاربران -

+ + + {/* Step Indicator */} +
+ +
- {/* Package Selector */} {token && ( )} - {/* Search Section - Only show if package is selected */} {selectedPackage && ( )} - {/* Messages */} {error && ( -
-
-
- - - -
-
-

{error}

-
-
+
+ + + +

{error}

+
)} - {success && ( -
-
-
- - - -
-
-

{success}

-
-
-
- )} - - {/* No Package Selected Message */} {!selectedPackage && packages.length === 0 && ( -
-

هیچ پکیجی یافت نشد. ابتدا یک پکیج ایجاد کنید.

+
+
+ + + +
+

هیچ پکیجی یافت نشد. ابتدا یک پکیج ایجاد کنید.

)} - {/* Show products list even without user */} {selectedPackage && products.length > 0 && !user && ( -
-
-
-

- محصولات موجود در پکیج {selectedPackage} -

-
-
-
- {products.map((product) => ( -
-

- {product.title || 'محصول'} -

-

- قیمت: {formatPrice(product.price)} تومان -

-

- نوع: {getProductTypeLabel(product.type)} -

-
- ))} -
-

- برای افزودن اشتراک به کاربر، ابتدا یک کاربر جستجو کنید -

+
+
+

+ محصولات موجود در پکیج {selectedPackage} +

+
+
+
+ {products.map((product) => ( +
+

+ {product.title || 'محصول'} +

+

+ قیمت: {formatPrice(product.price)} تومان +

+

+ نوع: {getProductTypeLabel(product.type)} +

+
+ ))}
+

+ برای افزودن اشتراک به کاربر، ابتدا یک کاربر جستجو کنید +

)} - {/* User Info */} {user && ( <> @@ -331,24 +288,23 @@ export default function UsersPage() {
)} - {/* User Products */} {user && selectedPackage && ( )} - {/* Edit User Modal */} {user && isEditing && (
); -} \ No newline at end of file +} diff --git a/app/globals.css b/app/globals.css index 5444c4d..82b69e0 100644 --- a/app/globals.css +++ b/app/globals.css @@ -42,14 +42,13 @@ } :root { - --background: #f3f4f6; - --foreground: #171717; + --background: #f9fafb; + --foreground: #111827; } -/* Dark palette is driven by the `.dark` class so the toggle controls it */ .dark { --background: #030712; - --foreground: #ededed; + --foreground: #f9fafb; } @theme inline { @@ -64,6 +63,8 @@ body { color: var(--foreground); font-family: 'Dana', 'Inter', Arial, Helvetica, sans-serif; font-weight: 400; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } /* Smooth theme transition for surfaces, borders and text */ @@ -71,7 +72,7 @@ body { *::before, *::after { transition-property: background-color, border-color, color, fill, stroke; - transition-duration: 200ms; + transition-duration: 150ms; transition-timing-function: ease; } @@ -84,8 +85,11 @@ body { scrollbar-color: #374151 transparent; } *::-webkit-scrollbar { - width: 10px; - height: 10px; + width: 8px; + height: 8px; +} +*::-webkit-scrollbar-track { + background: transparent; } *::-webkit-scrollbar-thumb { background-color: #cbd5e1; @@ -105,3 +109,24 @@ body { background-color: #4853c4; color: #f9fafb; } + +/* Toast animations */ +@keyframes fade-in { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes slide-in-from-top-4 { + from { transform: translateY(-1rem); } + to { transform: translateY(0); } +} +.animate-in { + animation-duration: 300ms; + animation-timing-function: ease-out; + animation-fill-mode: both; +} +.fade-in { + animation-name: fade-in; +} +.slide-in-from-top-4 { + animation-name: slide-in-from-top-4; +} diff --git a/components/AuthGuard.tsx b/components/AuthGuard.tsx new file mode 100644 index 0000000..559732a --- /dev/null +++ b/components/AuthGuard.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useAuth } from '@/contexts/AuthContext'; + +export default function AuthGuard({ children }: { children: React.ReactNode }) { + const { token, isLoading } = useAuth(); + const router = useRouter(); + + useEffect(() => { + if (!isLoading && !token) { + router.replace('/admin/login'); + } + }, [token, isLoading, router]); + + if (isLoading) { + return ( +
+
+
+

در حال بررسی دسترسی...

+
+
+ ); + } + + if (!token) { + return null; + } + + return <>{children}; +} diff --git a/components/Toast.tsx b/components/Toast.tsx new file mode 100644 index 0000000..a6f92f8 --- /dev/null +++ b/components/Toast.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { useEffect, useState, useCallback } from 'react'; +import { CheckCircleIcon, ExclamationCircleIcon, XMarkIcon } from '@heroicons/react/24/outline'; + +export interface Toast { + id: string; + type: 'success' | 'error'; + message: string; +} + +let toastListeners: ((toast: Toast) => void)[] = []; + +export function showToast(type: 'success' | 'error', message: string) { + const toast: Toast = { + id: Math.random().toString(36).slice(2), + type, + message, + }; + toastListeners.forEach((fn) => fn(toast)); +} + +export default function ToastContainer() { + const [toasts, setToasts] = useState([]); + + const addToast = useCallback((toast: Toast) => { + setToasts((prev) => [...prev, toast]); + setTimeout(() => { + setToasts((prev) => prev.filter((t) => t.id !== toast.id)); + }, 4000); + }, []); + + useEffect(() => { + toastListeners.push(addToast); + return () => { + toastListeners = toastListeners.filter((fn) => fn !== addToast); + }; + }, [addToast]); + + if (toasts.length === 0) return null; + + return ( +
+ {toasts.map((toast) => ( +
+ {toast.type === 'success' ? ( + + ) : ( + + )} +

{toast.message}

+ +
+ ))} +
+ ); +} diff --git a/components/admin/PageHeader.tsx b/components/admin/PageHeader.tsx new file mode 100644 index 0000000..f15e0a2 --- /dev/null +++ b/components/admin/PageHeader.tsx @@ -0,0 +1,25 @@ +'use client'; + +interface PageHeaderProps { + title: string; + subtitle?: string; + action?: React.ReactNode; +} + +export default function PageHeader({ title, subtitle, action }: PageHeaderProps) { + return ( +
+
+

+ {title} +

+ {subtitle && ( +

+ {subtitle} +

+ )} +
+ {action &&
{action}
} +
+ ); +} diff --git a/components/admin/Sidebar.tsx b/components/admin/Sidebar.tsx new file mode 100644 index 0000000..668f047 --- /dev/null +++ b/components/admin/Sidebar.tsx @@ -0,0 +1,132 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { useAuth } from '@/contexts/AuthContext'; +import { useState } from 'react'; +import { + UsersIcon, + CubeIcon, + TagIcon, + BellIcon, + GiftIcon, + PowerIcon, + ShoppingBagIcon, + HomeIcon, + Bars3Icon, + XMarkIcon, +} from '@heroicons/react/24/outline'; +import { MegaphoneIcon } from 'lucide-react'; +import ThemeToggle from '@/components/ThemeToggle'; + +const navItems = [ + { href: '/admin/dashboard', label: 'داشبورد', Icon: HomeIcon }, + { href: '/admin/users', label: 'کاربران', Icon: UsersIcon }, + { href: '/admin/purchases', label: 'خریدها', Icon: ShoppingBagIcon }, + { href: '/admin/packages', label: 'پکیج‌ها', Icon: CubeIcon }, + { href: '/admin/products', label: 'محصولات', Icon: TagIcon }, + { href: '/admin/reminders', label: 'یادآوری‌ها', Icon: BellIcon }, + { href: '/admin/promotions', label: 'تبلیغات', Icon: MegaphoneIcon }, + { href: '/admin/referral-rewards', label: 'پاداش معرفی', Icon: GiftIcon }, +]; + +export default function Sidebar() { + const pathname = usePathname(); + const { logout, user } = useAuth(); + const [mobileOpen, setMobileOpen] = useState(false); + + const isActive = (href: string) => { + if (href === '/admin/dashboard') return pathname === href; + return pathname.startsWith(href); + }; + + const navContent = ( +
+ {/* Logo / Brand */} +
+
+ A +
+
+

اپروایجنسی

+

پنل مدیریت

+
+
+ + {/* Navigation */} + + + {/* User, Theme & Logout */} +
+ + {user && ( +
+

{user.email}

+
+ )} + +
+
+ ); + + return ( + <> + {/* Mobile hamburger */} + + + {/* Mobile overlay */} + {mobileOpen && ( +
+
setMobileOpen(false)} + /> +
+ + {navContent} +
+
+ )} + + {/* Desktop sidebar */} + + + ); +} diff --git a/components/admin/StepIndicator.tsx b/components/admin/StepIndicator.tsx new file mode 100644 index 0000000..748bc52 --- /dev/null +++ b/components/admin/StepIndicator.tsx @@ -0,0 +1,61 @@ +'use client'; + +interface Step { + label: string; + isCompleted: boolean; + isCurrent: boolean; +} + +interface StepIndicatorProps { + steps: Step[]; +} + +export default function StepIndicator({ steps }: StepIndicatorProps) { + return ( +
+ {steps.map((step, index) => ( +
+
+
+ {step.isCompleted ? ( + + + + ) : ( + index + 1 + )} +
+ + {step.label} + +
+ {index < steps.length - 1 && ( +
+ )} +
+ ))} +
+ ); +} diff --git a/components/admin/packages/PackageForm.tsx b/components/admin/packages/PackageForm.tsx index 883ab4e..39a7758 100644 --- a/components/admin/packages/PackageForm.tsx +++ b/components/admin/packages/PackageForm.tsx @@ -37,7 +37,6 @@ export default function PackageForm({ package: pkg, onSubmit, onCancel, isLoadin web_app_url: pkg.web_app_url || '', tries: pkg.tries || 0, }); - if (pkg.image && typeof pkg.image === 'string') { setAvatarPreview(pkg.image); } @@ -46,18 +45,12 @@ export default function PackageForm({ package: pkg, onSubmit, onCancel, isLoadin const handleInputChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: value === '' ? null : value, - })); + setFormData(prev => ({ ...prev, [name]: value === '' ? null : value })); }; const handleNumberChange = (e: React.ChangeEvent) => { const { name, value } = e.target; - setFormData(prev => ({ - ...prev, - [name]: value === '' ? null : parseInt(value, 10), - })); + setFormData(prev => ({ ...prev, [name]: value === '' ? null : parseInt(value, 10) })); }; const handleAvatarChange = (e: React.ChangeEvent) => { @@ -65,12 +58,8 @@ export default function PackageForm({ package: pkg, onSubmit, onCancel, isLoadin if (file) { setAvatarFile(file); setFormData(prev => ({ ...prev, avatar: file })); - - // Create preview const reader = new FileReader(); - reader.onloadend = () => { - setAvatarPreview(reader.result as string); - }; + reader.onloadend = () => setAvatarPreview(reader.result as string); reader.readAsDataURL(file); } }; @@ -94,197 +83,89 @@ export default function PackageForm({ package: pkg, onSubmit, onCancel, isLoadin setFormData(prev => ({ ...prev, avatar: null })); }; + const inputClass = 'block w-full px-4 py-2.5 text-sm text-gray-900 dark:text-gray-100 border border-gray-200 dark:border-gray-700 rounded-xl bg-gray-50 dark:bg-gray-800/60 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500/20 focus:border-indigo-500 dark:focus:border-indigo-400 transition-colors'; + const labelClass = 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5'; + return ( -
-
- {/* Name */} + +
+

+ {pkg ? 'ویرایش پکیج' : 'ایجاد پکیج جدید'} +

+
+ +
- - + + +
+
+ +
- {/* Title */}
- - -
- - {/* Avatar */} -
- -
-