diff --git a/public/fonts/Vazirmatn-Bold.ttf b/public/fonts/Vazirmatn-Bold.ttf new file mode 100644 index 0000000..efa9b09 Binary files /dev/null and b/public/fonts/Vazirmatn-Bold.ttf differ diff --git a/public/fonts/Vazirmatn-Regular.ttf b/public/fonts/Vazirmatn-Regular.ttf new file mode 100644 index 0000000..1e08dd5 Binary files /dev/null and b/public/fonts/Vazirmatn-Regular.ttf differ diff --git a/scripts/generate-locales.js b/scripts/generate-locales.js index c27e4be..4206640 100644 --- a/scripts/generate-locales.js +++ b/scripts/generate-locales.js @@ -198,7 +198,7 @@ export const enLocale = { allServices: 'All Services', searchPlaceholder: 'Search services...', simcardTitle: 'SIM Card', - simcardDesc: 'Internet packages, international SIM cards, and traveler orders (Zain, Asiacell, Korek, STC)', + simcardDesc: 'Internet packages, international SIM cards, and traveler orders (Iran, Iraq, Saudi Arabia)', zayerGuideTitle: 'Zayer Guide', zayerGuideDesc: 'Holy shrines, historical sites, hospitals, mawakib, and city locations', exchangeTitle: 'Online Exchange', @@ -477,7 +477,7 @@ export const faLocale = { allServices: 'همه خدمات', searchPlaceholder: 'جستجوی خدمات...', simcardTitle: 'سیم‌کارت', - simcardDesc: 'مدیریت بسته‌های اینترنت، سیم‌کارت‌های بین‌المللی و سفارش‌های زائرین (زین، آسیاسل، کورک، STC)', + simcardDesc: 'مدیریت بسته‌های اینترنت، سیم‌کارت‌های بین‌المللی و سفارش‌های زائرین (ایران، عراق، عربستان سعودی)', zayerGuideTitle: 'راهنمای زائر', zayerGuideDesc: 'بانک جامع اماکن متبرکه، راهنمای اماکن تاریخی، بیمارستان‌ها، مواکب و موقعیت مکانی شهرها', exchangeTitle: 'صرافی آنلاین', @@ -756,7 +756,7 @@ export const arLocale = { allServices: 'جميع الخدمات', searchPlaceholder: 'البحث في الخدمات...', simcardTitle: 'شريحة الاتصال', - simcardDesc: 'إدارة باقات الإنترنت والشرائح الدولية وطلبات الزوار (زين، آسیاسیل، كورك، STC)', + simcardDesc: 'إدارة باقات الإنترنت والشرائح الدولية وطلبات الزوار (إيران، العراق، السعودية)', zayerGuideTitle: 'دليل الزائر', zayerGuideDesc: 'دليل شامل للعتبات المقدسة والمعالم التاريخية والمستشفيات والمواكب ومواقع المدن', exchangeTitle: 'الصرافة الإلكترونية', diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx index 8cd8747..d4ca9f3 100644 --- a/src/components/ui/checkbox.tsx +++ b/src/components/ui/checkbox.tsx @@ -6,7 +6,7 @@ function Checkbox({ className, ...props }: React.ComponentProps { - if (operatorFilter !== 'ALL' && !p.operator.toLowerCase().includes(operatorFilter.toLowerCase())) return false - if (searchTerm && !p.title.includes(searchTerm) && !p.operator.includes(searchTerm)) return false - return true - }) - - return ( -
- {/* آمار سیم‌کارت */} -
-
- سیم‌کارت‌های تحویل‌شده -
{toFa(524)}
-
-
- اپراتورهای پشتیبانی‌شده -
{toFa(5)}
-
-
- سفارش‌های در انتظار فعال‌سازی -
{toFa(12)}
-
-
- میانگین زمان تحویل در مرز/فرودگاه -
۱۰ دقیقه
-
-
- - {/* ابزارها و فیلتر */} -
-
- - - - -
-
- - setSearchTerm(e.target.value)} - placeholder="جستجوی بسته یا اپراتور..." - className="h-9 ps-9 text-xs bg-surface-base rounded-xl border-border-soft" - /> -
-
- - {/* جدول بسته‌های سیم‌کارت */} -
- - - - - - - - - - - - - - {filtered.map((item) => ( - - - - - - - - - - ))} - -
شناسهاپراتور و کشورعنوان بسته اینترنت و مکالمهمدت اعتبارقیمت رسمیموجودی انباروضعیت
#{item.id} -
- {item.operator} - {item.country} -
-
{item.title}{item.duration}{item.price}{item.stock} - - فعال در اپلیکیشن - -
-
-
- ) -} // ───────────────────────────────────────────────────────────────────────────── // ۳. صرافی آنلاین و تشریفات (Online Exchange) diff --git a/src/features/services/components/CreateLocationDialog.tsx b/src/features/services/components/CreateLocationDialog.tsx index 25eb624..904e65e 100644 --- a/src/features/services/components/CreateLocationDialog.tsx +++ b/src/features/services/components/CreateLocationDialog.tsx @@ -11,12 +11,14 @@ import { import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Ic } from '@/icons' -import type { +import { CityGuideItem, CityGuideCategory, CityGuideCountry, CreateCityGuideInput, } from '../types/zayer-guide' +import { ShimmerBox, ShimmerLine } from '@/components/shared/shimmer_frontend' +import { fetchCityGuideDetail } from '../services/zayer-guide-api' interface CreateLocationDialogProps { open: boolean @@ -38,6 +40,58 @@ const WEEK_DAYS = [ { code: 'FR', label: 'جمعه' }, ] +const CATEGORY_NAMES_FA: Record = { + mosque: 'مسجد و زیارتگاه', + pharmacy: 'داروخانه', + hospital: 'بیمارستان و درمانگاه', + souvenirs: 'سوغات و هدایا', + souvenir: 'سوغات و هدایا', + restaurant_fast_food: 'رستوران و فست‌فود', + supermarket: 'سوپرمارکت', + shopping_center: 'مراکز خرید', + cultural_entertainment_site: 'اماکن فرهنگی و گردشگری', +} + +const CITY_NAMES_FA: Record = { + mashhad: 'مشهد', + qom: 'قم', + karbala: 'کربلا', + najaf: 'نجف', + kufa: 'کوفه', + koufa: 'کوفه', + kadhimiya: 'کاظمین', + kazemein: 'کاظمین', + samarra: 'سامرا', + samara: 'سامرا', + mecca: 'مکه', + make: 'مکه', + medina: 'مدینه', + madineh: 'مدینه', + damascus: 'دمشق', +} + +const COUNTRY_NAMES_FA: Record = { + IR: 'ایران', + IQ: 'عراق', + SA: 'عربستان', + SY: 'سوریه', + OMN: 'عمان', + KWT: 'کویت', + BHR: 'بحرین', + Iran: 'ایران', + Iraq: 'عراق', + 'Saudi Arabia': 'عربستان سعودی', + Syria: 'سوریه', +} + +const LOCATION_LANGUAGES = [ + { code: 'ar', name: 'العربية (پیش‌فرض)', flag: '🇸🇦', dir: 'rtl' }, + { code: 'fa', name: 'فارسی', flag: '🇮🇷', dir: 'rtl' }, + { code: 'en', name: 'English', flag: '🇬🇧', dir: 'ltr' }, + { code: 'ru', name: 'Русский', flag: '🇷🇺', dir: 'ltr' }, + { code: 'id', name: 'Bahasa Indonesia', flag: '🇮🇩', dir: 'ltr' }, +] as const + export function CreateLocationDialog({ open, onOpenChange, @@ -47,23 +101,29 @@ export function CreateLocationDialog({ isSaving = false, onSave, }: CreateLocationDialogProps) { - // زبان فعال در تب‌های چندزبانه - const [activeLangTab, setActiveLangTab] = useState<'fa' | 'ar' | 'en'>('fa') + // زبان فعال در تب‌های چندزبانه (پیش‌فرض: عربی) + const [activeLangTab, setActiveLangTab] = useState<'ar' | 'fa' | 'en' | 'ru' | 'id'>('ar') // عناوین چندزبانه const [titleFa, setTitleFa] = useState('') const [titleAr, setTitleAr] = useState('') const [titleEn, setTitleEn] = useState('') + const [titleRu, setTitleRu] = useState('') + const [titleId, setTitleId] = useState('') // توضیحات چندزبانه const [descFa, setDescFa] = useState('') const [descAr, setDescAr] = useState('') const [descEn, setDescEn] = useState('') + const [descRu, setDescRu] = useState('') + const [descId, setDescId] = useState('') // آدرس‌های چندزبانه const [addrFa, setAddrFa] = useState('') const [addrAr, setAddrAr] = useState('') const [addrEn, setAddrEn] = useState('') + const [addrRu, setAddrRu] = useState('') + const [addrId, setAddrId] = useState('') // مشخصات عمومی و مکانی const [phoneNumber, setPhoneNumber] = useState('') @@ -77,60 +137,94 @@ export function CreateLocationDialog({ const [longitude, setLongitude] = useState('44.0244') const [imageUrl, setImageUrl] = useState('') const [validationError, setValidationError] = useState(null) + const [isLoadingDetail, setIsLoadingDetail] = useState(false) // لیست کلیه شهرهای تابعه کشور انتخابی const currentCountry = countries.find((c) => c.id === countryId) || countries[0] const availableCities = currentCountry?.city || [] useEffect(() => { - if (editingLocation) { - const trans = editingLocation.translations || {} + let isMounted = true - setTitleFa(trans.fa?.title || editingLocation.title || '') - setTitleAr(trans.ar?.title || '') + const applyLocationData = (loc: CityGuideItem) => { + const trans = loc.translations || {} + + setTitleAr(trans.ar?.title || loc.title || '') + setTitleFa(trans.fa?.title || '') setTitleEn(trans.en?.title || '') + setTitleRu(trans.ru?.title || '') + setTitleId(trans.id?.title || '') - setDescFa(trans.fa?.description || editingLocation.description || '') - setDescAr(trans.ar?.description || '') + setDescAr(trans.ar?.description || loc.description || '') + setDescFa(trans.fa?.description || '') setDescEn(trans.en?.description || '') + setDescRu(trans.ru?.description || '') + setDescId(trans.id?.description || '') - setAddrFa(trans.fa?.address || editingLocation.address || '') - setAddrAr(trans.ar?.address || '') + setAddrAr(trans.ar?.address || loc.address || '') + setAddrFa(trans.fa?.address || '') setAddrEn(trans.en?.address || '') - - setPhoneNumber(editingLocation.phone_number || '') - setCategoryId(editingLocation.category_id || editingLocation.category?.id || categories[0]?.id || 1) - setCountryId(editingLocation.country_id || editingLocation.country?.id || countries[0]?.id || 1) - setCityId(editingLocation.city_id || editingLocation.city?.id || 1) - setWorkingHoursFrom(editingLocation.working_hours_from ? editingLocation.working_hours_from.substring(0, 5) : '08:00') - setWorkingHoursTo(editingLocation.working_hours_to ? editingLocation.working_hours_to.substring(0, 5) : '23:00') - - const parsedDays = Array.isArray(editingLocation.days_off) - ? editingLocation.days_off - : typeof editingLocation.days_off === 'string' - ? (editingLocation.days_off as string).split(',').map((s) => s.trim()).filter(Boolean) + setAddrRu(trans.ru?.address || '') + setAddrId(trans.id?.address || '') + + setPhoneNumber(loc.phone_number || '') + setCategoryId(loc.category_id || loc.category?.id || categories[0]?.id || 1) + setCountryId(loc.country_id || loc.country?.id || countries[0]?.id || 1) + setCityId(loc.city_id || loc.city?.id || 1) + setWorkingHoursFrom(loc.working_hours_from ? loc.working_hours_from.substring(0, 5) : '08:00') + setWorkingHoursTo(loc.working_hours_to ? loc.working_hours_to.substring(0, 5) : '23:00') + + const parsedDays = Array.isArray(loc.days_off) + ? loc.days_off + : typeof loc.days_off === 'string' + ? (loc.days_off as string).split(',').map((s) => s.trim()).filter(Boolean) : [] setDaysOff(parsedDays) - setLatitude(String(editingLocation.latitude ?? '32.6160')) - setLongitude(String(editingLocation.longitude ?? '44.0244')) + setLatitude(String(loc.latitude ?? '32.6160')) + setLongitude(String(loc.longitude ?? '44.0244')) const img = - (typeof editingLocation.image?.image_url === 'object' - ? editingLocation.image?.image_url?.original - : editingLocation.image?.image_url) || '' + (typeof loc.image?.image_url === 'object' + ? loc.image?.image_url?.original + : loc.image?.image_url) || '' setImageUrl(typeof img === 'string' ? img : '') + } + + if (editingLocation) { + applyLocationData(editingLocation) + + // دریافت جزئیات کامل و ترجمه‌های پنج‌گانه از دیتابیس در صورت نیاز + if (!editingLocation.translations || Object.keys(editingLocation.translations).length < 4) { + setIsLoadingDetail(true) + fetchCityGuideDetail(editingLocation.slug || editingLocation.id) + .then((fullItem) => { + if (isMounted && fullItem) { + applyLocationData(fullItem) + } + }) + .catch((err) => console.warn('Could not fetch full translations:', err)) + .finally(() => { + if (isMounted) setIsLoadingDetail(false) + }) + } } else { // مقادیر پیش‌فرض برای ایجاد مکان جدید setTitleFa('') setTitleAr('') setTitleEn('') + setTitleRu('') + setTitleId('') setDescFa('') setDescAr('') setDescEn('') + setDescRu('') + setDescId('') setAddrFa('') setAddrAr('') setAddrEn('') + setAddrRu('') + setAddrId('') setPhoneNumber('') setCategoryId(categories[0]?.id || 1) const firstCountry = countries[0] @@ -148,6 +242,10 @@ export function CreateLocationDialog({ setImageUrl('') } setValidationError(null) + + return () => { + isMounted = false + } }, [editingLocation, open, categories, countries]) const toggleDayOff = (dayCode: string) => { @@ -159,8 +257,9 @@ export function CreateLocationDialog({ const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - // حداقل عنوان فارسی یا اصلی باید پر باشد - const mainTitle = titleFa.trim() || titleAr.trim() || titleEn.trim() + // حداقل یک عنوان معتبر وارد شده باشد + const mainTitle = + titleFa.trim() || titleAr.trim() || titleEn.trim() || titleRu.trim() || titleId.trim() if (!mainTitle) { setValidationError('لطفاً عنوان مکان را وارد کنید.') return @@ -177,21 +276,39 @@ export function CreateLocationDialog({ if (titleFa.trim()) titles.fa = titleFa.trim() if (titleAr.trim()) titles.ar = titleAr.trim() if (titleEn.trim()) titles.en = titleEn.trim() + if (titleRu.trim()) titles.ru = titleRu.trim() + if (titleId.trim()) titles.id = titleId.trim() const descriptions: Record = {} if (descFa.trim()) descriptions.fa = descFa.trim() if (descAr.trim()) descriptions.ar = descAr.trim() if (descEn.trim()) descriptions.en = descEn.trim() + if (descRu.trim()) descriptions.ru = descRu.trim() + if (descId.trim()) descriptions.id = descId.trim() const addresses: Record = {} if (addrFa.trim()) addresses.fa = addrFa.trim() if (addrAr.trim()) addresses.ar = addrAr.trim() if (addrEn.trim()) addresses.en = addrEn.trim() + if (addrRu.trim()) addresses.ru = addrRu.trim() + if (addrId.trim()) addresses.id = addrId.trim() const inputData: CreateCityGuideInput = { title: mainTitle, - description: descFa.trim() || descAr.trim() || descEn.trim() || '', - address: addrFa.trim() || addrAr.trim() || addrEn.trim() || '', + description: + descFa.trim() || + descAr.trim() || + descEn.trim() || + descRu.trim() || + descId.trim() || + '', + address: + addrFa.trim() || + addrAr.trim() || + addrEn.trim() || + addrRu.trim() || + addrId.trim() || + '', titles, descriptions, addresses, @@ -240,148 +357,236 @@ export function CreateLocationDialog({ {/* تب‌های انتخاب زبان برای محتوای متنی */}
-
-
اطلاعات متنی مکان
-
- - - +
+
+
اطلاعات متنی مکان
+ {isLoadingDetail && ( + + + در حال دریافت ترجمه‌ها از دیتابیس... + + )} +
+
+ {LOCATION_LANGUAGES.map((lang) => { + const isSelected = activeLangTab === lang.code + return ( + + ) + })}
- {/* بخش زبان فارسی */} - {activeLangTab === 'fa' && ( -
-
- - setTitleFa(e.target.value)} - placeholder="مثلاً: حرم مطهر امام حسین (ع) یا بیمارستان سفیر" - className="text-xs bg-surface-base" - required - /> -
-
- -