feat(admin): standardize Buttons and Modals across settings pages and media manager

This commit is contained in:
parsa aghaei 2026-08-23 10:17:04 +03:30
parent 16516e7003
commit 6de7d8be8a
8 changed files with 185 additions and 143 deletions

View File

@ -4,8 +4,10 @@ import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import PriceInput from '../components/ui/PriceInput';
import Button from '../components/ui/Button';
import type { FinancialSettings } from '../types/admin';
export default function FinancialSettingsPage() {
const [financial, setFinancial] = useState<FinancialSettings>({
taxPercentage: 10,
@ -169,13 +171,15 @@ export default function FinancialSettingsPage() {
onChange={(val) => setDonationInput(val)}
className="px-4 py-2 rounded-xl border border-gray-200 text-xs"
/>
<button
<Button
type="button"
variant="primary"
size="xs"
startIcon={Plus}
onClick={addDonationOption}
className="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-xl text-xs font-bold flex items-center gap-1 shrink-0 cursor-pointer"
>
<Plus className="w-4 h-4" /> افزودن
</button>
افزودن
</Button>
</div>
<div className="flex flex-wrap gap-2 pt-2">
@ -266,17 +270,19 @@ export default function FinancialSettingsPage() {
</div>
<div className="flex justify-end pt-4 border-t border-gray-100">
<button
<Button
type="submit"
disabled={isSaving}
className="bg-emerald-600 hover:bg-emerald-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-emerald-200 cursor-pointer"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
ذخیره تنظیمات مالی
</button>
</Button>
</div>
</form>
)}
</div>
);
}

View File

@ -6,6 +6,12 @@ import Spinner from '../components/ui/Spinner';
import Pagination from '../components/ui/Pagination';
import ConfirmModal from '../components/ui/ConfirmModal';
import ImagePreviewModal from '../components/ui/ImagePreviewModal';
import Button from '../components/ui/Button';
import Modal from '../components/ui/Modal';
interface Media {
id: string;
@ -299,26 +305,20 @@ export default function MediaManager() {
multiple
className="hidden"
/>
<button
<Button
variant="primary"
size="sm"
startIcon={Upload}
onClick={() => fileInputRef.current?.click()}
disabled={isUploading}
className="bg-purple-600 hover:bg-purple-700 disabled:opacity-70 text-white px-4 py-2.5 rounded-xl flex items-center gap-2 text-sm font-bold transition-all shadow-md shadow-purple-200 cursor-pointer"
isLoading={isUploading}
>
{isUploading ? (
<>
<Spinner size="sm" />
<span>در حال آپلود {uploadProgress ? `(${uploadProgress}٪)` : ''}...</span>
</>
) : (
<>
<Upload className="w-4 h-4" />
<span>آپلود فایل جدید (عکس، ویدئو، پادکست، PDF)</span>
</>
)}
</button>
{isUploading ? `در حال آپلود ${uploadProgress ? `(${uploadProgress}٪)` : ''}...` : 'آپلود فایل جدید'}
</Button>
</div>
</div>
{/* Upload Progress Bar if active */}
{isUploading && uploadProgress !== null && (
<div className="w-full bg-gray-100 rounded-full h-2 overflow-hidden shadow-inner">
@ -596,82 +596,82 @@ export default function MediaManager() {
</div>
)}
{/* Edit SEO Modal */}
{editingSeoMedia && (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-gray-900/60 backdrop-blur-xs">
<div className="bg-white rounded-2xl shadow-xl w-full max-w-md overflow-hidden p-6 space-y-4">
<div className="flex items-center justify-between border-b pb-3">
<h3 className="text-lg font-bold text-gray-900">تنظیمات سئوی تصویر</h3>
<button onClick={() => setEditingSeoMedia(null)} className="text-gray-400 hover:text-red-500">
<X className="w-5 h-5" />
</button>
</div>
<div className="space-y-3 text-right">
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">متن جایگزین (Alt Text) *</label>
<input
type="text"
value={seoFormData.altText}
onChange={(e) => setSeoFormData({ ...seoFormData, altText: e.target.value })}
placeholder="مثال: عکس مکمل کانیدروکس گپ کنینا"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">عنوان تصویر (Title)</label>
<input
type="text"
value={seoFormData.title}
onChange={(e) => setSeoFormData({ ...seoFormData, title: e.target.value })}
placeholder="عنوان تصویر برای تولتیپ هور"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">توضیحات (Description)</label>
<textarea
rows={2}
value={seoFormData.description}
onChange={(e) => setSeoFormData({ ...seoFormData, description: e.target.value })}
placeholder="توضیحات کامل سئو..."
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">زیرنویس (Caption)</label>
<input
type="text"
value={seoFormData.caption}
onChange={(e) => setSeoFormData({ ...seoFormData, caption: e.target.value })}
placeholder="متن کپشن زیر عکس در مقالات"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
</div>
<div className="flex justify-end gap-2 pt-2 border-t">
<button
type="button"
<Modal
isOpen={!!editingSeoMedia}
onClose={() => setEditingSeoMedia(null)}
title="تنظیمات سئوی تصویر"
icon={ImageIcon}
maxWidth="md"
footer={
<div className="flex justify-end gap-2.5 w-full">
<Button
variant="secondary"
size="sm"
onClick={() => setEditingSeoMedia(null)}
className="px-4 py-2 rounded-xl text-xs font-bold text-gray-600 bg-gray-100 hover:bg-gray-200"
>
انصراف
</button>
<button
type="button"
</Button>
<Button
variant="primary"
size="sm"
onClick={handleSaveSeo}
className="px-5 py-2 rounded-xl text-xs font-bold text-white bg-purple-600 hover:bg-purple-700 shadow-sm"
>
ذخیره سئو
</button>
</Button>
</div>
}
>
<div className="space-y-3 text-right font-vazir">
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">متن جایگزین (Alt Text) *</label>
<input
type="text"
value={seoFormData.altText}
onChange={(e) => setSeoFormData({ ...seoFormData, altText: e.target.value })}
placeholder="مثال: عکس مکمل کانیدروکس گپ کنینا"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">عنوان تصویر (Title)</label>
<input
type="text"
value={seoFormData.title}
onChange={(e) => setSeoFormData({ ...seoFormData, title: e.target.value })}
placeholder="عنوان تصویر برای تولتیپ هور"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">توضیحات (Description)</label>
<textarea
rows={2}
value={seoFormData.description}
onChange={(e) => setSeoFormData({ ...seoFormData, description: e.target.value })}
placeholder="توضیحات کامل سئو..."
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
<div>
<label className="text-xs font-bold text-gray-700 block mb-1">زیرنویس (Caption)</label>
<input
type="text"
value={seoFormData.caption}
onChange={(e) => setSeoFormData({ ...seoFormData, caption: e.target.value })}
placeholder="متن کپشن زیر عکس در مقالات"
className="w-full px-3 py-2 border rounded-xl text-sm"
/>
</div>
</div>
</div>
</Modal>
)}
<ConfirmModal
isOpen={!!deleteTargetId}
title="حذف تصویر"

View File

@ -4,8 +4,10 @@ import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import MediaSelector from '../components/ui/MediaSelector';
import Button from '../components/ui/Button';
import type { SeoSettings } from '../types/admin';
export default function SeoSettingsPage() {
const [seo, setSeo] = useState<SeoSettings>({
brandNameFa: 'کنینا ایران',
@ -271,31 +273,34 @@ export default function SeoSettingsPage() {
className="flex-1 px-4 py-3 rounded-xl border border-gray-200 focus:border-purple-500 outline-none text-xs font-mono"
dir="ltr"
/>
<button
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setIsMediaSelectorOpen(true)}
className="px-3 py-3 bg-purple-50 text-purple-600 hover:bg-purple-100 rounded-xl font-bold text-xs shrink-0 cursor-pointer"
>
انتخاب از رسانه
</button>
</Button>
</div>
</div>
</div>
</div>
<div className="flex justify-end pt-4 border-t border-gray-100">
<button
<Button
type="submit"
disabled={isSaving}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
ذخیره تنظیمات سئو و ساختار عناوین
</button>
</Button>
</div>
</form>
)}
{isMediaSelectorOpen && (
<MediaSelector
isOpen={isMediaSelectorOpen}

View File

@ -20,6 +20,9 @@ import {
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
export default function Settings() {
const [activeTab, setActiveTab] = useState<'brand' | 'contact' | 'links'>('brand');
@ -358,14 +361,15 @@ export default function Settings() {
</div>
<div className="flex justify-end">
<button
<Button
type="submit"
disabled={isSaving}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
<span>ذخیره هویت برند</span>
</button>
ذخیره هویت برند
</Button>
</div>
</form>
) : activeTab === 'contact' ? (
@ -468,16 +472,18 @@ export default function Settings() {
</div>
<div className="flex justify-end">
<button
<Button
type="submit"
disabled={isSaving}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
<span>ذخیره اطلاعات تماس</span>
</button>
ذخیره اطلاعات تماس
</Button>
</div>
</form>
) : (
/* Tab 3: Dedicated Modules Grid */
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-5">

View File

@ -4,6 +4,9 @@ import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import PriceInput from '../components/ui/PriceInput';
import Button from '../components/ui/Button';
export default function ShippingSettingsPage() {
const [shipping, setShipping] = useState({
@ -148,17 +151,19 @@ export default function ShippingSettingsPage() {
</div>
<div className="flex justify-end">
<button
<Button
type="submit"
disabled={isSaving}
className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-blue-200 cursor-pointer"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
<span>ذخیره تغییرات ارسال</span>
</button>
ذخیره تغییرات ارسال
</Button>
</div>
</form>
)}
</div>
);
}

View File

@ -39,6 +39,9 @@ import {
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
interface SmsConfigState {
enabled: boolean;
@ -767,17 +770,19 @@ export default function SmsSettingsPage() {
{/* Submit Button */}
<div className="flex justify-end pt-2">
<button
<Button
type="submit"
disabled={isSaving}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3.5 px-8 rounded-xl transition-all flex items-center gap-2 shadow-lg shadow-purple-200 disabled:opacity-50"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
ذخیره تنظیمات درگاه پیامک
</button>
</Button>
</div>
</form>
{/* Balance & Test SMS Sidebar (1 col on large) */}
<div className="space-y-6">
{/* Realtime Credit / Balance Card */}

View File

@ -19,6 +19,9 @@ import {
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
interface SslStatus {
exists: boolean;
@ -434,14 +437,16 @@ export default function SslSettingsPage() {
</div>
<div className="flex justify-end gap-3 pt-2">
<button
<Button
type="submit"
disabled={isUpdating || !certFile || !keyFile}
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-xl text-xs font-bold transition-all shadow-md shadow-purple-200 flex items-center gap-2 cursor-pointer"
variant="primary"
size="sm"
startIcon={ShieldCheck}
disabled={!certFile || !keyFile}
isLoading={isUpdating}
>
{isUpdating ? <Spinner size="sm" /> : <ShieldCheck className="w-4 h-4" />}
<span>نصب و فعالسازی گواهی جدید</span>
</button>
نصب و فعالسازی گواهی جدید
</Button>
</div>
</form>
) : (
@ -479,14 +484,16 @@ export default function SslSettingsPage() {
</div>
<div className="flex justify-end gap-3 pt-2">
<button
<Button
type="submit"
disabled={isUpdating || !certText.trim() || !keyText.trim()}
className="px-6 py-2.5 bg-purple-600 hover:bg-purple-700 disabled:opacity-50 text-white rounded-xl text-xs font-bold transition-all shadow-md shadow-purple-200 flex items-center gap-2 cursor-pointer"
variant="primary"
size="sm"
startIcon={ShieldCheck}
disabled={!certText.trim() || !keyText.trim()}
isLoading={isUpdating}
>
{isUpdating ? <Spinner size="sm" /> : <ShieldCheck className="w-4 h-4" />}
<span>ذخیره و اعمال تغییرات</span>
</button>
ذخیره و اعمال تغییرات
</Button>
</div>
</form>
)}
@ -509,15 +516,18 @@ export default function SslSettingsPage() {
className="flex-1 px-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-xs font-mono outline-none focus:border-blue-500"
dir="ltr"
/>
<button
<Button
type="button"
variant="outline"
size="sm"
startIcon={ExternalLink}
onClick={handleTestOnline}
disabled={isTestingOnline}
className="px-5 py-2.5 bg-blue-600 hover:bg-blue-700 text-white text-xs font-bold rounded-xl transition-all shadow-xs flex items-center justify-center gap-2 cursor-pointer"
isLoading={isTestingOnline}
>
{isTestingOnline ? <Spinner size="sm" /> : <ExternalLink className="w-4 h-4" />}
<span>تست زنده پورت ۴۴۳ دامنه</span>
</button>
تست زنده پورت ۴۴۳ دامنه
</Button>
</div>
</div>
{onlineResult && (

View File

@ -14,8 +14,11 @@ import {
import { toast } from 'react-hot-toast';
import api from '../services/api';
import Spinner from '../components/ui/Spinner';
import Button from '../components/ui/Button';
import type { SystemSettings } from '../types/admin';
export default function SystemSettingsPage() {
const [system, setSystem] = useState<SystemSettings>({
maintenanceMode: false,
@ -324,17 +327,19 @@ export default function SystemSettingsPage() {
</div>
<div className="flex justify-end">
<button
<Button
type="submit"
disabled={isSaving}
className="bg-purple-600 hover:bg-purple-700 text-white font-bold py-3 px-8 rounded-xl transition-colors flex items-center gap-2 shadow-md shadow-purple-200 cursor-pointer"
variant="primary"
size="md"
startIcon={Save}
isLoading={isSaving}
>
{isSaving ? <Spinner size="sm" /> : <Save className="w-5 h-5" />}
<span>ذخیره تنظیمات سیستمی</span>
</button>
ذخیره تنظیمات سیستمی
</Button>
</div>
</form>
)}
</div>
);
}