canina/frontend/admin-panel/src/pages/UITexts.tsx
2026-08-18 20:08:07 +03:30

1110 lines
46 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useRef } from 'react';
import {
Languages,
Save,
Search,
Loader2,
Layout,
Sliders,
Link as LinkIcon,
FileText,
Check,
Upload,
HelpCircle,
Eye,
Layers,
ChevronDown,
Sparkles,
Home,
ShoppingBag,
BookOpen,
PhoneCall,
Building2,
Heart,
Smile,
Globe,
Settings,
Pill,
ShieldCheck,
HeartPulse,
Activity,
Award,
Video,
Info,
} from 'lucide-react';
import { toast } from 'react-hot-toast';
import api, { BASE_DOMAIN } from '../services/api';
import Spinner from '../components/ui/Spinner';
import ToggleSwitch from '../components/ui/ToggleSwitch';
import ImagePreviewModal from '../components/ui/ImagePreviewModal';
import MediaSelector from '../components/ui/MediaSelector';
import RichTextEditor from '../components/ui/RichTextEditor';
import IconPickerModal, { ICON_REGISTRY } from '../components/ui/IconPickerModal';
// Classic v1 Groupings
const V1_PAGE_TABS = [
{ id: 'all', label: 'همه بخش‌ها', icon: Layout },
{ id: 'hero', label: 'بنر اصلی (Hero)', icon: Sliders },
{ id: 'header', label: 'هدر و دکمه‌ها', icon: LinkIcon },
{ id: 'advisor', label: 'دستیار سلامت', icon: HelpCircle },
{ id: 'videos', label: 'ویدئوها & CTA', icon: FileText },
{ id: 'footer', label: 'فوتور و ارتباطات', icon: Languages },
];
const V1_GROUPS: Record<string, string[]> = {
'تنظیمات کاتالوگ‌مود و حالت صیانت': [
'maintenance_mode',
'maintenance_title',
'maintenance_desc',
'maintenance_eta',
'maintenance_contact_phone',
'maintenance_badge',
'catalog_mode',
'catalog_hide_prices',
'catalog_disable_cart',
'catalog_disable_checkout',
],
'هدر و ناوبری': [
'shipping_notice',
'brand_name_fa',
'brand_subtitle',
'nav_solutions',
'nav_products',
'nav_wiki',
'nav_blog',
'nav_pet_profiles',
'nav_search_placeholder',
'nav_search_button',
'nav_pet_switcher_title',
'nav_register_pet',
'nav_wallet_orders',
'nav_logout',
'nav_login',
'nav_cart',
],
'بنر اصلی (Hero)': [
'hero_badge',
'hero_title',
'hero_desc',
'hero_btn_advisor',
'hero_btn_products',
'hero_image_badge',
'hero_image_title',
'hero_quality_standard',
'hero_image_url',
'hero_stat_founded',
'hero_stat_agencies',
'hero_stat_german_formula',
],
'دستیار سلامت': [
'advisor_badge',
'advisor_title',
'advisor_subtitle',
'advisor_step1_title',
'advisor_step1_desc',
'advisor_pet_name_label',
'advisor_pet_name_placeholder',
'advisor_pet_breed_label',
'advisor_pet_breed_placeholder',
'advisor_submit_btn',
'advisor_validation_warning',
'advisor_step2_title',
'advisor_step2_desc',
'advisor_age_label',
'advisor_weight_label',
'advisor_activity_label',
'advisor_back',
'advisor_next_final',
'advisor_step3_title',
'advisor_step3_desc',
'advisor_complete',
],
'محصولات برگزیده': [
'featured_badge',
'featured_title_line1',
'featured_title_line2',
'featured_desc',
'featured_view_all',
'product_view_details',
'featured_no_products_title',
'featured_no_products_desc',
],
'ویدئوها': [
'videos_badge',
'videos_title',
'videos_title_highlight',
'videos_desc',
'videos_view_all',
],
'کارت‌های ناوبری': [
'nav_card_shop_title',
'nav_card_shop_desc',
'nav_card_shop_cta',
'nav_card_wiki_title',
'nav_card_wiki_desc',
'nav_card_wiki_cta',
],
'درباره ما': [
'about_label',
'about_title',
'about_item1_title',
'about_item1_desc',
'about_item2_title',
'about_item2_desc',
'about_item3_title',
'about_item3_desc',
],
'CTA': [
'cta_title',
'cta_desc',
'cta_btn_primary',
'cta_btn_secondary',
],
'فوتر': [
'footer_brand_title',
'footer_brand_subtitle',
'footer_brand_desc',
'footer_links_title',
'footer_link_products',
'footer_link_blog',
'footer_link_profiles',
'footer_link_wiki',
'footer_link_b2b',
'footer_contact_title',
'footer_phone_label',
'footer_phone',
'footer_email_label',
'footer_email',
'footer_address_label',
'footer_address',
'footer_auth_title',
'footer_auth_desc',
'footer_auth_badge',
'footer_copyright',
'footer_about',
'footer_contact',
'footer_privacy',
],
};
const V1_GROUP_PAGE_MAP: Record<string, string> = {
'بنر اصلی (Hero)': 'hero',
'هدر و ناوبری': 'header',
'دستیار سلامت': 'advisor',
'ویدئوها': 'videos',
'CTA': 'videos',
'کارت‌های ناوبری': 'hero',
'فوتر': 'footer',
};
// Structured Page Builder v2 Definitions
interface SectionField {
key: string;
label: string;
type: 'rich-text' | 'text' | 'image' | 'icon' | 'boolean';
description?: string;
defaultVal?: string;
}
interface PageSection {
id: string;
title: string;
icon: React.ComponentType<{ className?: string }>;
fields: SectionField[];
}
interface AppSitePage {
id: string;
title: string;
path: string;
icon: React.ComponentType<{ className?: string }>;
sections: PageSection[];
}
const SITE_PAGES: AppSitePage[] = [
{
id: 'home',
title: 'صفحه اصلی (Home Page)',
path: '/',
icon: Home,
sections: [
{
id: 'hero',
title: 'بنر اصلی (Hero Banner)',
icon: Sliders,
fields: [
{ key: 'hero_badge', label: 'بج بالای تیتر بنر', type: 'text' },
{ key: 'hero_title', label: 'عنوان اصلی بنر (H1 سئو)', type: 'rich-text', description: 'تگ‌های H1 یا رنگ کلمات کلیدی را مشخص کنید' },
{ key: 'hero_desc', label: 'متن توضیحات بنر', type: 'rich-text' },
{ key: 'hero_btn_advisor', label: 'متن دکمه دستیار سلامت', type: 'text' },
{ key: 'hero_btn_products', label: 'متن دکمه کاتالوگ محصولات', type: 'text' },
{ key: 'hero_image_url', label: 'تصویر پس‌زمینه یا گرافیک بنر اصلی', type: 'image' },
{ key: 'hero_image_title', label: 'متن روی کارت تصویر', type: 'text' },
{ key: 'hero_image_badge', label: 'بج روی کارت تصویر', type: 'text' },
{ key: 'hero_icon_badge', label: 'آیکون بج بنر', type: 'icon' },
{ key: 'hero_stat_founded', label: 'آمار قدمت کنینا (سال تأسیس)', type: 'text' },
{ key: 'hero_stat_agencies', label: 'آمار نمایندگی‌ها', type: 'text' },
{ key: 'hero_stat_german_formula', label: 'آمار فرمولاسیون آلمانی', type: 'text' },
],
},
{
id: 'featured',
title: 'محصولات برگزیده (Featured Products)',
icon: ShoppingBag,
fields: [
{ key: 'featured_badge', label: 'بج بالای سکشن', type: 'text' },
{ key: 'featured_title_line1', label: 'عنوان سطر اول محصولات برگزیده (H2)', type: 'rich-text' },
{ key: 'featured_title_line2', label: 'عنوان سطر دوم محصولات برگزیده', type: 'rich-text' },
{ key: 'featured_desc', label: 'توضیحات معرفی مکمل‌ها', type: 'rich-text' },
{ key: 'featured_view_all', label: 'متن دکمه مشاهده همه محصولات', type: 'text' },
],
},
{
id: 'nav_cards',
title: 'کارت‌های ناوبری سریع (Navigation Cards)',
icon: Layout,
fields: [
{ key: 'nav_card_shop_title', label: 'عنوان کارت فروشگاه', type: 'text' },
{ key: 'nav_card_shop_desc', label: 'توضیح کارت فروشگاه', type: 'text' },
{ key: 'nav_card_shop_cta', label: 'دکمه اقدام کارت فروشگاه', type: 'text' },
{ key: 'nav_card_wiki_title', label: 'عنوان کارت دانشنامه', type: 'text' },
{ key: 'nav_card_wiki_desc', label: 'توضیح کارت دانشنامه', type: 'text' },
{ key: 'nav_card_wiki_cta', label: 'دکمه اقدام کارت دانشنامه', type: 'text' },
],
},
{
id: 'videos',
title: 'ویدئوها و آموزش‌ها (Videos Showcase)',
icon: Video,
fields: [
{ key: 'videos_badge', label: 'بج بالای بخش ویدئوها', type: 'text' },
{ key: 'videos_title', label: 'عنوان بخش ویدئوها (H2)', type: 'rich-text' },
{ key: 'videos_title_highlight', label: 'متن هایلایت‌شده عنوان ویدئو', type: 'text' },
{ key: 'videos_desc', label: 'توضیحات کوتاه بخش ویدئوها', type: 'rich-text' },
{ key: 'videos_view_all', label: 'متن دکمه مشاهده همه ویدئوها', type: 'text' },
],
},
{
id: 'about',
title: 'درباره اصالت و کیفیت (About & Standards)',
icon: Award,
fields: [
{ key: 'about_label', label: 'برچسب بالای بخش', type: 'text' },
{ key: 'about_title', label: 'عنوان اصلی درباره کنینا', type: 'rich-text' },
{ key: 'about_item1_title', label: 'عنوان ویژگی اول (استاندارد دارویی)', type: 'text' },
{ key: 'about_item1_desc', label: 'توضیحات ویژگی اول', type: 'rich-text' },
{ key: 'about_item2_title', label: 'عنوان ویژگی دوم (فرمولاسیون تخصصی)', type: 'text' },
{ key: 'about_item2_desc', label: 'توضیحات ویژگی دوم', type: 'rich-text' },
{ key: 'about_item3_title', label: 'عنوان ویژگی سوم (تاییدیه دامپزشکان)', type: 'text' },
{ key: 'about_item3_desc', label: 'توضیحات ویژگی سوم', type: 'rich-text' },
],
},
{
id: 'cta',
title: 'باکس دعوت به اقدام (Call To Action)',
icon: Sparkles,
fields: [
{ key: 'cta_title', label: 'عنوان اصلی کادر دعوت به خرید (H2)', type: 'rich-text' },
{ key: 'cta_desc', label: 'متن توضیحات و ترغیب', type: 'rich-text' },
{ key: 'cta_btn_primary', label: 'متن دکمه اصلی (خرید مکمل)', type: 'text' },
{ key: 'cta_btn_secondary', label: 'متن دکمه ثانویه (مشاوره دامپزشک)', type: 'text' },
],
},
],
},
{
id: 'shop',
title: 'فروشگاه و کاتالوگ (Shop & Catalog)',
path: '/shop',
icon: ShoppingBag,
sections: [
{
id: 'shop_header',
title: 'هدر و فیلترهای فروشگاه',
icon: Sliders,
fields: [
{ key: 'shop_page_title', label: 'عنوان صفحه فروشگاه (H1)', type: 'rich-text' },
{ key: 'shop_page_desc', label: 'توضیحات سئو بالای فروشگاه', type: 'rich-text' },
{ key: 'shop_filter_categories', label: 'عنوان فیلتر دسته‌بندی‌ها', type: 'text' },
{ key: 'shop_filter_price', label: 'عنوان فیلتر محدوده قیمت', type: 'text' },
{ key: 'shop_search_placeholder', label: 'متن کادر جستجوی محصولات', type: 'text' },
],
},
],
},
{
id: 'advisor',
title: 'مشاور هوشمند سلامت پت (Smart Advisor)',
path: '/#canino-advisor',
icon: HelpCircle,
sections: [
{
id: 'advisor_general',
title: 'عناوین و مراحل دستیار هوشمند',
icon: HelpCircle,
fields: [
{ key: 'advisor_badge', label: 'بج بالای مشاور هوشمند', type: 'text' },
{ key: 'advisor_title', label: 'عنوان مشاور هوشمند (H2)', type: 'rich-text' },
{ key: 'advisor_subtitle', label: 'زیرعنوان و شعار تشخیصی', type: 'rich-text' },
{ key: 'advisor_step1_title', label: 'عنوان مرحله ۱ (مشخصات پت)', type: 'text' },
{ key: 'advisor_step1_desc', label: 'توضیحات مرحله ۱', type: 'text' },
{ key: 'advisor_pet_name_label', label: 'برچسب نام پت', type: 'text' },
{ key: 'advisor_pet_breed_label', label: 'برچسب نژاد پت', type: 'text' },
{ key: 'advisor_step2_title', label: 'عنوان مرحله ۲ (سن، وزن و تحرک)', type: 'text' },
{ key: 'advisor_step2_desc', label: 'توضیحات مرحله ۲', type: 'text' },
{ key: 'advisor_step3_title', label: 'عنوان مرحله ۳ (علائم و پیشنهاد)', type: 'text' },
{ key: 'advisor_step3_desc', label: 'توضیحات مرحله ۳', type: 'text' },
{ key: 'advisor_submit_btn', label: 'متن دکمه مشاهده راهکار پیشنهادی', type: 'text' },
],
},
],
},
{
id: 'wiki_blog',
title: 'دانشنامه و وبلاگ تخصصی (Wiki & Blog)',
path: '/wiki',
icon: BookOpen,
sections: [
{
id: 'wiki_general',
title: 'عناوین و صفحات دانشنامه',
icon: BookOpen,
fields: [
{ key: 'wiki_page_title', label: 'عنوان صفحه دانشنامه (H1)', type: 'rich-text' },
{ key: 'wiki_page_desc', label: 'توضیحات سئو دانشنامه دارویی', type: 'rich-text' },
{ key: 'blog_page_title', label: 'عنوان صفحه مقالات و وبلاگ', type: 'rich-text' },
{ key: 'blog_page_desc', label: 'توضیحات سئو وبلاگ', type: 'rich-text' },
],
},
],
},
{
id: 'contact_about',
title: 'درباره ما و تماس با ما (About & Contact)',
path: '/contact',
icon: PhoneCall,
sections: [
{
id: 'contact_info',
title: 'اطلاعات تماس و پیام‌ها',
icon: PhoneCall,
fields: [
{ key: 'contact_page_title', label: 'عنوان صفحه تماس با ما', type: 'rich-text' },
{ key: 'contact_page_desc', label: 'توضیحات بالای فرم تماس', type: 'rich-text' },
{ key: 'about_full_history', label: 'تاریخچه کامل برند و شرکت (H2/P)', type: 'rich-text' },
],
},
],
},
{
id: 'b2b',
title: 'پرتال همکاران عمده‌فروشی (B2B Portal)',
path: '/b2b',
icon: Building2,
sections: [
{
id: 'b2b_landing',
title: 'صفحه معرفی و شرایط همکاری B2B',
icon: Building2,
fields: [
{ key: 'b2b_hero_title', label: 'عنوان بنر همکاری B2B (H1)', type: 'rich-text' },
{ key: 'b2b_hero_desc', label: 'توضیحات و مزایای همکاری تجاری', type: 'rich-text' },
{ key: 'b2b_form_title', label: 'عنوان فرم درخواست نمایندگی', type: 'text' },
{ key: 'b2b_form_desc', label: 'راهنمای ارسال مدارک داروخانه/کلینیک', type: 'rich-text' },
],
},
],
},
{
id: 'pet_profiles',
title: 'پرونده سلامت همدم (Pet Profiles)',
path: '/profile/pets',
icon: Heart,
sections: [
{
id: 'pet_profiles_info',
title: 'عناوین پرونده سلامت و یادآورها',
icon: Heart,
fields: [
{ key: 'pet_profile_title', label: 'عنوان بخش پرونده سلامت پت', type: 'rich-text' },
{ key: 'pet_profile_desc', label: 'توضیحات پرونده و برنامه تغذیه', type: 'rich-text' },
],
},
],
},
];
const SHARED_HEADER_FIELDS: SectionField[] = [
{ key: 'shipping_notice', label: 'متن نوار متحرک بالای سایت (Ticker Announcement)', type: 'rich-text', description: 'نمایش در بالاترین نوار زرد تمام صفحات' },
{ key: 'brand_name_fa', label: 'نام فارسی برند در هدر', type: 'text' },
{ key: 'brand_subtitle', label: 'زیرعنوان نمایندگی در هدر', type: 'text' },
{ key: 'nav_solutions', label: 'عنوان منوی راهکارهای درمانی', type: 'text' },
{ key: 'nav_products', label: 'عنوان منوی کاتالوگ محصولات', type: 'text' },
{ key: 'nav_wiki', label: 'عنوان منوی دانشنامه', type: 'text' },
{ key: 'nav_blog', label: 'عنوان منوی وبلاگ', type: 'text' },
{ key: 'nav_pet_profiles', label: 'عنوان منوی پرونده سلامت', type: 'text' },
{ key: 'nav_search_placeholder', label: 'متن پیش‌فرض کادر جستجو', type: 'text' },
{ key: 'nav_cart', label: 'متن دکمه سبد خرید', type: 'text' },
{ key: 'header_logo_icon', label: 'آیکون نماد هدر', type: 'icon' },
];
const SHARED_FOOTER_FIELDS: SectionField[] = [
{ key: 'footer_brand_title', label: 'عنوان برند در فوتر', type: 'text' },
{ key: 'footer_brand_subtitle', label: 'زیرعنوان برند در فوتر', type: 'text' },
{ key: 'footer_brand_desc', label: 'متن معرفی شرکت در فوتر (سئو فوتر)', type: 'rich-text' },
{ key: 'footer_links_title', label: 'عنوان ستون دسترسی سریع', type: 'text' },
{ key: 'footer_contact_title', label: 'عنوان ستون راه‌های ارتباطی', type: 'text' },
{ key: 'footer_phone_label', label: 'برچسب تلفن پشتیبانی', type: 'text' },
{ key: 'footer_email_label', label: 'برچسب ایمیل', type: 'text' },
{ key: 'footer_address_label', label: 'برچسب آدرس دفتر', type: 'text' },
{ key: 'footer_copyright', label: 'متن حق نشر و کپی‌رایت انتهای فوتر', type: 'rich-text' },
{ key: 'footer_auth_badge', label: 'متن بج اصالت آلمان', type: 'text' },
];
export default function UITexts() {
const [versionTab, setVersionTab] = useState<'v2' | 'v1'>('v2');
const [v2SubTab, setV2SubTab] = useState<'header' | 'footer' | 'pages'>('pages');
const [openPageId, setOpenPageId] = useState<string>('home');
const [openSectionId, setOpenSectionId] = useState<string>('hero');
// State
const [texts, setTexts] = useState<Record<string, string>>({});
const [edits, setEdits] = useState<Record<string, string>>({});
const [search, setSearch] = useState('');
const [savingKey, setSavingKey] = useState<string | null>(null);
const [savedKey, setSavedKey] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [isSavingAll, setIsSavingAll] = useState(false);
// Modals
const [previewModalUrl, setPreviewModalUrl] = useState<string | null>(null);
const [mediaSelectorKey, setMediaSelectorKey] = useState<string | null>(null);
const [iconPickerKey, setIconPickerKey] = useState<string | null>(null);
// Legacy v1 State
const [v1ActiveTab, setV1ActiveTab] = useState('all');
useEffect(() => {
let isSubscribed = true;
api.get('/settings/ui-texts')
.then((res) => {
if (!isSubscribed) return;
const textsObj: Record<string, string> = {};
if (Array.isArray(res.data)) {
res.data.forEach((item: { key?: string; value?: string }) => {
if (item && item.key) {
textsObj[item.key] = typeof item.value === 'string' ? item.value : (item.value ? JSON.stringify(item.value) : '');
}
});
} else if (typeof res.data === 'object' && res.data !== null) {
Object.assign(textsObj, res.data);
}
setTexts(textsObj);
})
.catch((e) => {
console.error('Failed to load UI texts:', e);
})
.finally(() => {
if (isSubscribed) setLoading(false);
});
return () => {
isSubscribed = false;
};
}, []);
const handleSave = async (key: string) => {
const value = edits[key] ?? texts[key] ?? '';
setSavingKey(key);
try {
await api.put(`/settings/ui-texts/${encodeURIComponent(key)}`, { value });
setTexts((prev) => ({ ...prev, [key]: value }));
setSavedKey(key);
toast.success('محتوا با موفقیت ذخیره شد');
setTimeout(() => setSavedKey(null), 2000);
} catch {
try {
await api.patch(`/settings/ui-texts/${encodeURIComponent(key)}`, { value });
setTexts((prev) => ({ ...prev, [key]: value }));
setSavedKey(key);
toast.success('محتوا با موفقیت ذخیره شد');
setTimeout(() => setSavedKey(null), 2000);
} catch (err: any) {
const errorMsg = err?.response?.data?.message || 'خطا در ذخیره‌سازی محتوا';
toast.error(errorMsg);
}
} finally {
setSavingKey(null);
}
};
const handleSaveAll = async () => {
const dirtyKeys = Object.keys(edits);
if (dirtyKeys.length === 0) {
toast.info('هیچ تغییری برای ذخیره وجود ندارد');
return;
}
setIsSavingAll(true);
try {
await api.put('/settings/ui-texts', edits);
setTexts((prev) => ({ ...prev, ...edits }));
setEdits({});
toast.success(`${dirtyKeys.length} تغییر با موفقیت ذخیره شد`);
} catch {
try {
const promises = dirtyKeys.map((k) =>
api.put(`/settings/ui-texts/${encodeURIComponent(k)}`, { value: edits[k] })
);
await Promise.all(promises);
setTexts((prev) => ({ ...prev, ...edits }));
setEdits({});
toast.success(`${dirtyKeys.length} تغییر با موفقیت ذخیره شد`);
} catch {
toast.error('خطا در ذخیره‌سازی گروهی تغییرات');
}
} finally {
setIsSavingAll(false);
}
};
const isImageKey = (k: string) => k.includes('image') || k.includes('logo') || k.includes('img') || k.includes('avatar') || k.includes('url');
if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<Spinner size="lg" className="text-purple-600" />
</div>
);
}
const hasUnsavedChanges = Object.keys(edits).length > 0;
// Render a Single Field in Page Builder
const renderField = (field: SectionField) => {
const val = edits[field.key] ?? texts[field.key] ?? field.defaultVal ?? '';
const isSaved = savedKey === field.key;
const isSaving = savingKey === field.key;
return (
<div key={field.key} className="bg-gray-50/70 p-5 rounded-2xl border border-gray-200/80 space-y-3">
<div className="flex items-center justify-between">
<div>
<span className="text-sm font-black text-gray-900 block">{field.label}</span>
{field.description && (
<span className="text-[11px] text-gray-500 font-medium block mt-0.5">{field.description}</span>
)}
<code className="text-[10px] text-purple-600 font-mono font-bold block mt-0.5" dir="ltr">
{field.key}
</code>
</div>
{isSaved && (
<span className="text-xs text-emerald-600 font-black flex items-center gap-1 bg-emerald-50 px-2.5 py-1 rounded-lg border border-emerald-200">
<Check className="w-3.5 h-3.5" /> ذخیره شد
</span>
)}
</div>
{/* Input by Type */}
{field.type === 'rich-text' ? (
<RichTextEditor
value={val}
onChange={(html) => setEdits({ ...edits, [field.key]: html })}
placeholder="متن خود را وارد کنید..."
/>
) : field.type === 'image' ? (
<div className="space-y-3">
<div className="flex gap-2">
<input
type="text"
value={val}
onChange={(e) => setEdits({ ...edits, [field.key]: e.target.value })}
placeholder="https://... یا انتخاب از گالری"
className="flex-1 px-4 py-2.5 bg-white border border-gray-200 rounded-xl text-xs font-mono font-bold outline-none focus:ring-2 focus:ring-purple-500"
dir="ltr"
/>
<button
type="button"
onClick={() => setMediaSelectorKey(field.key)}
className="px-4 py-2.5 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-bold transition-all flex items-center gap-1.5 shadow-sm cursor-pointer"
>
<Upload className="w-4 h-4" />
<span>گالری و آپلود</span>
</button>
</div>
{val && (
<div className="flex items-center gap-3 p-3 bg-white rounded-xl border border-gray-200">
<div className="w-16 h-16 rounded-lg bg-gray-50 border border-gray-100 overflow-hidden shrink-0 flex items-center justify-center p-1">
<img
src={val.startsWith('http') ? val : `${BASE_DOMAIN}${val}`}
alt="Preview"
className="max-w-full max-h-full object-contain"
/>
</div>
<div className="flex-1 truncate text-xs text-gray-600 font-mono" dir="ltr">
{val}
</div>
<button
type="button"
onClick={() => setPreviewModalUrl(val.startsWith('http') ? val : `${BASE_DOMAIN}${val}`)}
className="p-2 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-lg transition-colors cursor-pointer"
title="مشاهده بزرگ‌تر"
>
<Eye className="w-4 h-4" />
</button>
</div>
)}
</div>
) : field.type === 'icon' ? (
<div className="flex items-center gap-3 bg-white p-3 rounded-xl border border-gray-200">
<div className="w-12 h-12 rounded-xl bg-purple-50 text-purple-600 border border-purple-200 flex items-center justify-center font-black shrink-0">
{ICON_REGISTRY[val] ? (
React.createElement(ICON_REGISTRY[val], { className: 'w-6 h-6' })
) : (
<Sparkles className="w-6 h-6 text-gray-400" />
)}
</div>
<div className="flex-1">
<span className="text-xs font-bold text-gray-900 block">
{val ? `آیکون انتخاب‌شده: ${val}` : 'هیچ آیکونی انتخاب نشده است'}
</span>
<span className="text-[11px] text-gray-400 font-mono block">از کتابخانه Lucide Icons</span>
</div>
<button
type="button"
onClick={() => setIconPickerKey(field.key)}
className="px-4 py-2 bg-purple-50 hover:bg-purple-100 text-purple-700 border border-purple-200 rounded-xl text-xs font-black transition-all cursor-pointer"
>
انتخاب آیکون
</button>
</div>
) : field.type === 'boolean' ? (
<div className="flex items-center justify-between p-3 bg-white rounded-xl border border-gray-200">
<span className="text-xs font-bold text-gray-700">
وضعیت: {val === 'true' ? <strong className="text-green-600">فعال (روشن)</strong> : <strong className="text-gray-400">غیرفعال (خاموش)</strong>}
</span>
<ToggleSwitch
checked={val === 'true'}
onChange={(c) => setEdits({ ...edits, [field.key]: String(c) })}
/>
</div>
) : (
<input
type="text"
value={val}
onChange={(e) => setEdits({ ...edits, [field.key]: e.target.value })}
className="w-full px-4 py-2.5 bg-white border border-gray-200 rounded-xl text-xs font-bold text-gray-900 outline-none focus:ring-2 focus:ring-purple-500"
/>
)}
<div className="flex justify-end pt-1">
<button
type="button"
onClick={() => handleSave(field.key)}
disabled={isSaving}
className="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-xs font-black transition-all flex items-center gap-1.5 shadow-sm cursor-pointer"
>
{isSaving ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Save className="w-3.5 h-3.5" />}
<span>ذخیره این فیلد</span>
</button>
</div>
</div>
);
};
return (
<div className="space-y-6 font-vazir text-right" dir="rtl">
{/* Top Header */}
<div className="bg-white p-6 rounded-3xl border border-gray-200 shadow-xs flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-black text-gray-900 flex items-center gap-3">
<Languages className="w-7 h-7 text-purple-600" />
مدیریت محتوا و ویرایشگر صفحات (Page Builder v2)
</h1>
<p className="text-xs text-gray-500 font-bold mt-1">
ویرایشگر جامع و زنده متون سئو، تگهای عناوین، تصاویر بنرها و آیکونهای لوسید در تمام بخشها
</p>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={handleSaveAll}
disabled={isSavingAll || !hasUnsavedChanges}
className={`px-5 py-2.5 rounded-xl text-xs font-black flex items-center gap-2 transition-all shadow-sm ${
hasUnsavedChanges
? 'bg-purple-600 hover:bg-purple-700 text-white shadow-purple-200 cursor-pointer animate-pulse'
: 'bg-gray-100 text-gray-400 cursor-not-allowed'
}`}
>
{isSavingAll ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
<span>ذخیره تمامی تغییرات {hasUnsavedChanges ? `(${Object.keys(edits).length})` : ''}</span>
</button>
</div>
</div>
{/* Main Version Switcher: v2 (New Structure) vs v1 (Legacy) */}
<div className="flex items-center justify-between bg-white p-2 rounded-2xl border border-gray-200 shadow-xs">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => setVersionTab('v2')}
className={`px-5 py-2.5 rounded-xl text-xs font-black transition-all flex items-center gap-2 cursor-pointer ${
versionTab === 'v2'
? 'bg-gradient-to-r from-purple-600 to-indigo-600 text-white shadow-md shadow-purple-200'
: 'text-gray-600 hover:bg-gray-100'
}`}
>
<Sparkles className="w-4 h-4 text-amber-300" />
<span>ویرایشگر پیشرفته صفحات (Page Builder v2)</span>
</button>
<button
type="button"
onClick={() => setVersionTab('v1')}
className={`px-4 py-2.5 rounded-xl text-xs font-bold transition-all flex items-center gap-2 cursor-pointer ${
versionTab === 'v1'
? 'bg-gray-800 text-white shadow-sm'
: 'text-gray-500 hover:bg-gray-100'
}`}
>
<span>نمای کلاسیک (v1 Legacy)</span>
</button>
</div>
<div className="text-[11px] text-gray-400 font-bold px-3">
{versionTab === 'v2' ? 'نسخه مدرن آکاردئونی تفکیک‌شده' : 'نسخه پشتیبان فهرستی'}
</div>
</div>
{versionTab === 'v2' ? (
<div className="space-y-6">
{/* Sub Tabs: Header, Footer, Site Pages */}
<div className="flex items-center gap-2 bg-white p-2 rounded-2xl border border-gray-200 shadow-xs overflow-x-auto no-scrollbar">
<button
type="button"
onClick={() => setV2SubTab('pages')}
className={`px-6 py-3 rounded-xl text-xs font-black transition-all flex items-center gap-2 cursor-pointer shrink-0 ${
v2SubTab === 'pages'
? 'bg-purple-600 text-white shadow-md shadow-purple-200'
: 'text-gray-600 hover:bg-gray-50 hover:text-purple-700'
}`}
>
<Layout className="w-4 h-4" />
<span>صفحات سایت (Site Pages Accordion)</span>
</button>
<button
type="button"
onClick={() => setV2SubTab('header')}
className={`px-6 py-3 rounded-xl text-xs font-black transition-all flex items-center gap-2 cursor-pointer shrink-0 ${
v2SubTab === 'header'
? 'bg-purple-600 text-white shadow-md shadow-purple-200'
: 'text-gray-600 hover:bg-gray-50 hover:text-purple-700'
}`}
>
<LinkIcon className="w-4 h-4" />
<span>هدر و ناوبری مشترک (Header)</span>
</button>
<button
type="button"
onClick={() => setV2SubTab('footer')}
className={`px-6 py-3 rounded-xl text-xs font-black transition-all flex items-center gap-2 cursor-pointer shrink-0 ${
v2SubTab === 'footer'
? 'bg-purple-600 text-white shadow-md shadow-purple-200'
: 'text-gray-600 hover:bg-gray-50 hover:text-purple-700'
}`}
>
<Languages className="w-4 h-4" />
<span>فوتر و کپیرایت مشترک (Footer)</span>
</button>
</div>
{/* Sub Tab: Shared Header */}
{v2SubTab === 'header' && (
<div className="bg-white p-6 sm:p-8 rounded-3xl border border-gray-200 shadow-sm space-y-6">
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-2xl bg-purple-50 text-purple-600 flex items-center justify-center font-black">
<LinkIcon className="w-5 h-5" />
</div>
<div>
<h3 className="text-base font-black text-gray-900">تنظیمات هدر، اعلان بالا و منوها</h3>
<p className="text-xs text-gray-500 font-medium">محتوای مشترک بالای تمام صفحات فروشگاه</p>
</div>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2">
{SHARED_HEADER_FIELDS.map((f) => renderField(f))}
</div>
</div>
)}
{/* Sub Tab: Shared Footer */}
{v2SubTab === 'footer' && (
<div className="bg-white p-6 sm:p-8 rounded-3xl border border-gray-200 shadow-sm space-y-6">
<div className="flex items-center justify-between border-b border-gray-100 pb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-2xl bg-purple-50 text-purple-600 flex items-center justify-center font-black">
<Languages className="w-5 h-5" />
</div>
<div>
<h3 className="text-base font-black text-gray-900">تنظیمات فوتر، کپیرایت و ستونهای پیوند</h3>
<p className="text-xs text-gray-500 font-medium">محتوای مشترک انتهای تمام صفحات سایت</p>
</div>
</div>
</div>
<div className="grid gap-6 md:grid-cols-2">
{SHARED_FOOTER_FIELDS.map((f) => renderField(f))}
</div>
</div>
)}
{/* Sub Tab: Site Pages Accordion */}
{v2SubTab === 'pages' && (
<div className="space-y-4">
{SITE_PAGES.map((page) => {
const PageIcon = page.icon;
const isOpen = openPageId === page.id;
const activeSection = page.sections.find((s) => s.id === openSectionId) || page.sections[0];
return (
<div
key={page.id}
className="bg-white rounded-3xl border border-gray-200 shadow-xs overflow-hidden transition-all"
>
{/* Collapsible Page Header */}
<button
type="button"
onClick={() => setOpenPageId(isOpen ? '' : page.id)}
className={`w-full p-5 sm:p-6 flex items-center justify-between transition-colors cursor-pointer text-right ${
isOpen ? 'bg-purple-50/70 border-b border-purple-100' : 'hover:bg-gray-50'
}`}
>
<div className="flex items-center gap-3.5">
<div
className={`w-10 h-10 rounded-2xl flex items-center justify-center transition-all ${
isOpen
? 'bg-purple-600 text-white shadow-md shadow-purple-200'
: 'bg-gray-100 text-gray-600'
}`}
>
<PageIcon className="w-5 h-5" />
</div>
<div>
<h3 className="text-base font-black text-gray-900">{page.title}</h3>
<span className="text-xs text-purple-600 font-mono font-bold" dir="ltr">
{page.path}
</span>
</div>
</div>
<div className="flex items-center gap-3">
<span className="text-xs font-bold px-3 py-1 bg-white rounded-xl border border-gray-200 text-gray-600">
{page.sections.length} بخش
</span>
<ChevronDown
className={`w-5 h-5 text-gray-400 transition-transform duration-200 ${
isOpen ? 'rotate-180 text-purple-600' : ''
}`}
/>
</div>
</button>
{/* Page Content & Section Tabs */}
{isOpen && (
<div className="p-6 sm:p-8 space-y-6">
{/* Section Tabs */}
{page.sections.length > 1 && (
<div className="flex items-center gap-2 overflow-x-auto pb-2 border-b border-gray-100 no-scrollbar">
{page.sections.map((sec) => {
const SecIcon = sec.icon;
const isSecActive = (openSectionId || page.sections[0].id) === sec.id;
return (
<button
key={sec.id}
type="button"
onClick={() => setOpenSectionId(sec.id)}
className={`px-4 py-2 rounded-xl text-xs font-black transition-all flex items-center gap-2 cursor-pointer shrink-0 border ${
isSecActive
? 'bg-purple-600 text-white border-purple-600 shadow-sm'
: 'bg-gray-50 text-gray-700 border-gray-200 hover:bg-gray-100'
}`}
>
<SecIcon className="w-3.5 h-3.5" />
<span>{sec.title}</span>
</button>
);
})}
</div>
)}
{/* Active Section Fields Grid */}
<div className="space-y-4">
<div className="flex items-center gap-2 pb-2">
<span className="w-2 h-2 rounded-full bg-purple-600" />
<h4 className="text-sm font-black text-gray-800">
فیلدهای محتوایی: {activeSection.title}
</h4>
</div>
<div className="grid gap-6 md:grid-cols-2">
{activeSection.fields.map((f) => renderField(f))}
</div>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
) : (
/* Legacy v1 Interface */
<div className="space-y-6">
{/* Search */}
<div className="bg-white p-4 rounded-2xl border border-gray-200 shadow-xs flex items-center gap-3">
<Search className="w-5 h-5 text-gray-400 shrink-0" />
<input
type="text"
placeholder="جستجو در متون نسخه ۱..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full bg-transparent border-none text-xs focus:outline-none font-bold"
/>
</div>
{/* Section Tabs v1 */}
<div className="flex items-center gap-2 overflow-x-auto pb-2 no-scrollbar">
{V1_PAGE_TABS.map((tab) => {
const Icon = tab.icon;
const isActive = v1ActiveTab === tab.id;
return (
<button
key={tab.id}
onClick={() => setV1ActiveTab(tab.id)}
className={`px-4 py-2.5 rounded-xl text-xs font-black transition-all shrink-0 flex items-center gap-2 border cursor-pointer ${
isActive
? 'bg-purple-600 text-white border-purple-600 shadow-md'
: 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
}`}
>
<Icon className="w-4 h-4" />
<span>{tab.label}</span>
</button>
);
})}
</div>
{/* Content Groups v1 */}
<div className="space-y-6">
{Object.entries(V1_GROUPS).map(([groupTitle, keys]) => {
const mappedTab = V1_GROUP_PAGE_MAP[groupTitle] || 'other';
if (v1ActiveTab !== 'all' && mappedTab !== v1ActiveTab) return null;
const filteredKeys = keys.filter((key) => {
if (!search) return true;
const currentVal = edits[key] ?? texts[key] ?? '';
return key.toLowerCase().includes(search.toLowerCase()) || currentVal.includes(search);
});
if (filteredKeys.length === 0) return null;
return (
<div
key={groupTitle}
className="bg-white rounded-3xl border border-gray-200 p-6 space-y-4 shadow-xs"
>
<h3 className="text-base font-black text-gray-900 border-b border-gray-100 pb-3 flex items-center gap-2">
<span className="w-2.5 h-2.5 rounded-full bg-purple-600 inline-block" />
{groupTitle}
</h3>
<div className="grid gap-4 md:grid-cols-2">
{filteredKeys.map((key) => {
const val = edits[key] ?? texts[key] ?? '';
const isImg = isImageKey(key);
return (
<div key={key} className="space-y-2 p-4 bg-gray-50/70 rounded-2xl border border-gray-100">
<label className="text-[11px] font-mono text-purple-700 font-bold block" dir="ltr">
{key}
</label>
<div className="flex items-center gap-2">
{val.length > 60 && !isImg ? (
<textarea
value={val}
onChange={(e) => setEdits({ ...edits, [key]: e.target.value })}
className="w-full bg-white border border-gray-200 rounded-xl p-3 text-xs font-bold text-gray-900 focus:outline-none focus:ring-2 focus:ring-purple-500/20 h-20"
/>
) : (
<input
type="text"
value={val}
onChange={(e) => setEdits({ ...edits, [key]: e.target.value })}
className="w-full bg-white border border-gray-200 rounded-xl px-3 py-2 text-xs font-bold text-gray-900 focus:outline-none focus:ring-2 focus:ring-purple-500/20"
/>
)}
<button
type="button"
onClick={() => handleSave(key)}
className="px-3.5 py-2 bg-purple-600 text-white hover:bg-purple-700 rounded-xl text-xs font-black transition-all shrink-0 shadow-xs cursor-pointer"
>
ذخیره
</button>
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
</div>
)}
{/* Media Selector Modal */}
{mediaSelectorKey && (
<MediaSelector
isOpen={!!mediaSelectorKey}
onClose={() => setMediaSelectorKey(null)}
onSelect={(url) => {
setEdits({ ...edits, [mediaSelectorKey]: url });
setMediaSelectorKey(null);
}}
/>
)}
{/* Icon Picker Modal */}
{iconPickerKey && (
<IconPickerModal
isOpen={!!iconPickerKey}
onClose={() => setIconPickerKey(null)}
currentIcon={edits[iconPickerKey] ?? texts[iconPickerKey]}
onSelect={(iconName) => {
setEdits({ ...edits, [iconPickerKey]: iconName });
setIconPickerKey(null);
}}
/>
)}
{/* Image Preview Modal */}
<ImagePreviewModal
isOpen={!!previewModalUrl}
imageUrl={previewModalUrl || ''}
onClose={() => setPreviewModalUrl(null)}
/>
</div>
);
}