Browse Source

refactor: update tour feature management and add inline translation variant

master
parent
commit
b209b5fbd3
  1. 16
      .agents/rules/iconography_standard.md
  2. 2
      .agents/rules/post_code_change_verification.md
  3. 2
      AGENTS.md
  4. 70
      src/components/shared/ServiceTranslation/ServiceTranslation.tsx
  5. 2
      src/components/shared/ServiceTranslation/types.ts
  6. 247
      src/components/shared/TourFeatureIconPicker.tsx
  7. 274
      src/features/tours/components/CreateTourDialog.tsx
  8. 381
      src/features/tours/components/TourDetailDialog.tsx
  9. 4
      src/features/tours/services/tour-api.ts
  10. 5
      src/features/tours/types.ts
  11. 68
      src/icons.tsx

16
.agents/rules/iconography_standard.md

@ -0,0 +1,16 @@
# Iconography & Visual Assets Standard - Aqila Admin Panel
این سند استاندارد و قوانین قطعی استفاده از آیکون‌ها در پنل مدیریت ادمین (`aqila_panel`) را تبیین می‌کند.
---
## ۱. اصل منبع واحد برای آیکون‌ها (Single Source of Truth)
- کلیه آیکون‌های سایدبار، نوار بالایی، جداول و فرم‌ها باید از کتابخانه آیکون رسمی و مصوب پنل (نظیر Heroicons یا Lucide متناسب با معماری پنل) به صورت یکدست ایمپورت شوند.
- استفاده از آیکون‌های پراکنده با ضخامت‌های خط (stroke) متفاوت اکیداً ممنوع است.
---
## ۲. سایزبندی و ترازبندی استاندارد
- آیکون‌های سایدبار و نوار ناوبری: `w-5 h-5` (۲۰ پیکسل).
- آیکون‌های داخل دکمه‌های جدول و اکشن‌ها: `w-4 h-4` (۱۶ پیکسل).
- رنگ و استیت‌های فعال، هاور و غیرفعال باید کاملاً منطبق بر سیستم رنگ سازمانی پنل باشند.

2
.agents/rules/post_code_change_verification.md

@ -11,7 +11,7 @@
--- ---
## ۲. چک‌لیست اعتبارسنجی در پنل مدیریت ## ۲. چک‌لیست اعتبارسنجی در پنل مدیریت
1. **کامپایل تایپ‌اسکریپت و بیلد Vite:** تغییرات نباید باعث خطای تایپ یا کرش HMR در سرور در حال اجرا شوند.
1. **کامپایل تایپ‌اسکریپت و بیلد Vite:** تغییرات نباید باعث خطای تایپ یا کرش HMR در سرور در حال اجرا شوند. همچنین از اجرای چند سرور توسعه موازی روی یک محیط کاری یا ایمپورت Barrelهای سنگین که موجب اختلال در بارگذاری چانک‌ها می‌شوند خودداری گردد.
2. **سلامت جریان داده‌ها در جداول و فرم‌ها:** بررسی صحت مدیریت مقادیر نال/آندفایند در کامپوننت‌های داده‌محور (Data Tables, Dropdowns, Forms) تا در مواجهه با دیتای دریافتی کرش نکنند. 2. **سلامت جریان داده‌ها در جداول و فرم‌ها:** بررسی صحت مدیریت مقادیر نال/آندفایند در کامپوننت‌های داده‌محور (Data Tables, Dropdowns, Forms) تا در مواجهه با دیتای دریافتی کرش نکنند.
3. **سازگاری با سرویس‌های API:** تطابق فراخوانی متدهای رکوئست با مدل‌ها و پاسخ‌های بک‌اند. 3. **سازگاری با سرویس‌های API:** تطابق فراخوانی متدهای رکوئست با مدل‌ها و پاسخ‌های بک‌اند.

2
AGENTS.md

@ -66,6 +66,6 @@
- **شرط اختصاصی اعمال (صرفاً پس از تغییر کد - عدم اعمال در چت‌های عادی):** این بررسی و هرگونه اعتبارسنجی سلامت ران‌تایم، صرفاً و منحصراً در پیام‌ها و تسک‌هایی که کدی در آن ویرایش یا ایجاد شده باشد اعمال می‌شود. در چت‌ها و پیام‌های عادی که تغییری در کدها داده نشده است، اجرای این بررسی‌ها اکیداً لازم نیست. - **شرط اختصاصی اعمال (صرفاً پس از تغییر کد - عدم اعمال در چت‌های عادی):** این بررسی و هرگونه اعتبارسنجی سلامت ران‌تایم، صرفاً و منحصراً در پیام‌ها و تسک‌هایی که کدی در آن ویرایش یا ایجاد شده باشد اعمال می‌شود. در چت‌ها و پیام‌های عادی که تغییری در کدها داده نشده است، اجرای این بررسی‌ها اکیداً لازم نیست.
- **بررسی سریع و بلادرنگ ران بودن پنل مدیریت (Immediate Runtime & Health Check):** - **بررسی سریع و بلادرنگ ران بودن پنل مدیریت (Immediate Runtime & Health Check):**
- بلافاصله بررسی شود که تغییرات در کامپوننت‌ها، هوک‌ها، تایپ‌ها یا استایل‌ها باعث خطای کامپایل TypeScript، خطای بیلد Vite یا کرش داشبورد نشده باشد. - بلافاصله بررسی شود که تغییرات در کامپوننت‌ها، هوک‌ها، تایپ‌ها یا استایل‌ها باعث خطای کامپایل TypeScript، خطای بیلد Vite یا کرش داشبورد نشده باشد.
- سلامت ران بودن با بررسی تایپ‌ها (`npm run typecheck` / `tsc --noEmit`) یا وضعیت سرور Vite تضمین شود.
- سلامت ران بودن با بررسی تایپ‌ها (`npm run typecheck` / `tsc --noEmit`) یا وضعیت سرور Vite تضمین شود و از اجرای همزمان چند سرور توسعه موازی یا ایمپورت پکیج‌های Barrel سنگین که باعث کندی HMR یا خطای بارگذاری چانک می‌شوند خودداری گردد.
- **اصل بازیابی فوری و خودکار (Instant Self-Healing):** - **اصل بازیابی فوری و خودکار (Instant Self-Healing):**
- در صورتی که تغییر کد باعث بروز هرگونه خطا، شکست کامپایل یا توقف ران بودن پنل شود، ایجنت موظف است فوراً منشأ ارور را شناسایی و اصلاح کند تا پنل مدیریت مجدداً در حالت ران، کاملاً سالم و پایدار قرار گیرد. - در صورتی که تغییر کد باعث بروز هرگونه خطا، شکست کامپایل یا توقف ران بودن پنل شود، ایجنت موظف است فوراً منشأ ارور را شناسایی و اصلاح کند تا پنل مدیریت مجدداً در حالت ران، کاملاً سالم و پایدار قرار گیرد.

70
src/components/shared/ServiceTranslation/ServiceTranslation.tsx

@ -40,6 +40,76 @@ export function ServiceTranslation({
return Boolean(dirtyLangs[code]) return Boolean(dirtyLangs[code])
} }
if (variant === 'inline') {
return (
<div className={`flex items-center gap-1.5 shrink-0 ${className}`}>
{/* آیکون یا عنوان زبان */}
{showGlobeIcon && (
<div className="flex items-center gap-1 text-xs font-bold text-muted-foreground shrink-0 select-none">
<Ic name="globe" className="size-3.5 text-primary shrink-0" />
{title && <span className="hidden xl:inline text-[11px]">{title}:</span>}
{required && <span className="text-rose-500">*</span>}
</div>
)}
{/* دکمه‌های پیل زبان */}
<div className="flex items-center h-8.5 gap-0.5 p-0.5 rounded-xl bg-surface-base border border-border-soft shrink-0">
{SERVICE_LANGUAGES.map((lang) => {
const isSelected = activeLang === lang.code
const hasData = isCompleted(lang.code)
const dirty = isDirty(lang.code)
const displayName = lang.code === 'id' ? 'Indonesia' : lang.name
return (
<button
key={lang.code}
type="button"
onClick={() => onLangChange(lang.code)}
className={`h-7.5 relative px-2 text-[11px] rounded-lg transition-colors font-medium flex items-center gap-1 shrink-0 cursor-pointer ${
isSelected
? 'bg-primary text-white shadow-xs font-bold'
: hasData
? 'bg-card text-foreground hover:bg-card/80 border border-border-soft'
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
}`}
title={lang.label}
>
<span className="text-xs leading-none">{lang.flag}</span>
<span>{displayName}</span>
{hasData && (
<span
className={`size-1.5 rounded-full shrink-0 ${
isSelected ? 'bg-white' : 'bg-emerald-500'
}`}
/>
)}
{dirty && (
<span className="size-1.5 rounded-full bg-amber-400 animate-pulse shrink-0" />
)}
</button>
)
})}
</div>
{/* دایرکشن متن */}
{showDirectionBadge && (
<div
className={`h-7.5 px-1.5 flex items-center justify-center rounded-lg font-bold text-[9px] uppercase border shrink-0 font-mono ${
currentLang.dir === 'rtl'
? 'bg-amber-500/10 text-amber-600 dark:text-amber-400 border-amber-500/20'
: 'bg-blue-500/10 text-blue-600 dark:text-blue-400 border-blue-500/20'
}`}
title={`جهت متن: ${currentLang.dir.toUpperCase()}`}
>
{currentLang.dir.toUpperCase()}
</div>
)}
{children}
</div>
)
}
return ( return (
<div className={`space-y-3 ${className}`}> <div className={`space-y-3 ${className}`}>
{/* هدر انتخاب زبان و نمایش دایرکشن */} {/* هدر انتخاب زبان و نمایش دایرکشن */}

2
src/components/shared/ServiceTranslation/types.ts

@ -69,6 +69,6 @@ export interface ServiceTranslationProps {
showGlobeIcon?: boolean showGlobeIcon?: boolean
required?: boolean required?: boolean
className?: string className?: string
variant?: 'grid' | 'pills'
variant?: 'grid' | 'pills' | 'inline'
children?: React.ReactNode children?: React.ReactNode
} }

247
src/components/shared/TourFeatureIconPicker.tsx

@ -0,0 +1,247 @@
import React, { useState, useMemo } from 'react'
import { Popover, PopoverTrigger, PopoverContent } from '@/components/ui/popover'
import { Input } from '@/components/ui/input'
import { Ic, type IconName } from '@/icons'
import { cn } from '@/lib/utils'
export interface FeatureIconDef {
name: IconName
labelFa: string
category: 'stay' | 'transport' | 'food' | 'ziyarat' | 'staff' | 'finance' | 'gear' | 'leisure'
keywords: string[]
}
export const TOUR_FEATURE_ICONS: FeatureIconDef[] = [
// ۱. اقامت و هتل
{ name: 'hotel', labelFa: 'هتل و اقامتگاه', category: 'stay', keywords: ['هتل', 'hotel', 'اقامت', 'residence'] },
{ name: 'hotelStar', labelFa: 'هتل ۵ ستاره', category: 'stay', keywords: ['ستاره', '5star', 'ممتاز', 'لوکس'] },
{ name: 'bed', labelFa: 'تخت‌خواب / اتاق', category: 'stay', keywords: ['تخت', 'bed', 'اتاق', 'room'] },
{ name: 'twinBed', labelFa: 'اتاق دوتخته', category: 'stay', keywords: ['دوتخته', 'twin', 'سوئیت'] },
{ name: 'roomKey', labelFa: 'کلید سوئیت', category: 'stay', keywords: ['کلید', 'key', 'سوئیت', 'تحویل'] },
{ name: 'roomService', labelFa: 'خدمات اتاق و پذیرش', category: 'stay', keywords: ['خدمات', 'room service', 'بل بوی', 'پذیرش'] },
{ name: 'airCondition', labelFa: 'تهویه و سرمایش/گرمایش', category: 'stay', keywords: ['کولر', 'اسپلیت', 'تهویه', 'air condition'] },
{ name: 'view', labelFa: 'ویو و چشم‌انداز حرم', category: 'stay', keywords: ['ویو', 'دید', 'چشم انداز', 'حرم'] },
{ name: 'safeBox', labelFa: 'صندوق امانات اتاق', category: 'stay', keywords: ['گاوصندوق', 'صندوق', 'safe'] },
{ name: 'building', labelFa: 'برج و ساختمان هتل', category: 'stay', keywords: ['ساختمان', 'برج', 'هتل'] },
// ۲. حمل‌ونقل و ترانسفر
{ name: 'busVip', labelFa: 'اتوبوس VIP توریستی', category: 'transport', keywords: ['اتوبوس', 'bus', 'vip', 'ترانسفر'] },
{ name: 'bus', labelFa: 'اتوبوس استاندارد', category: 'transport', keywords: ['اتوبوس', 'سفر زمینی', 'bus'] },
{ name: 'van', labelFa: 'ون تشریفاتی', category: 'transport', keywords: ['ون', 'van', 'مینی بوس'] },
{ name: 'airplane', labelFa: 'پرواز و هواپیما', category: 'transport', keywords: ['پرواز', 'هواپیما', 'plane', 'flight'] },
{ name: 'plane', labelFa: 'بلیط پرواز', category: 'transport', keywords: ['هواپیما', 'پرواز'] },
{ name: 'airportShuttle', labelFa: 'ترانسفر فرودگاهی', category: 'transport', keywords: ['فرودگاه', 'ترانسفر', 'shuttle'] },
{ name: 'train', labelFa: 'قطار بین‌شهری', category: 'transport', keywords: ['قطار', 'train', 'راه آهن'] },
{ name: 'taxi', labelFa: 'تاکسی دربستی', category: 'transport', keywords: ['تاکسی', 'taxi', 'خودرو'] },
{ name: 'car', labelFa: 'ماشین اختصاصی', category: 'transport', keywords: ['خودرو', 'ماشین', 'car'] },
{ name: 'ship', labelFa: 'ترانسفر دریایی / کشتی', category: 'transport', keywords: ['کشتی', 'دریایی', 'قایق', 'ship'] },
// ۳. پذیرایی و غذا
{ name: 'buffet', labelFa: 'بوفه باز سلف سرویس', category: 'food', keywords: ['بوفه', 'سلف', 'buffet', 'پذیرایی'] },
{ name: 'meal', labelFa: 'سه وعده غذا', category: 'food', keywords: ['غذا', 'رستوران', 'meal', 'خوراک'] },
{ name: 'breakfast', labelFa: 'صبحانه هتل', category: 'food', keywords: ['صبحانه', 'breakfast', 'چای', 'قهوه'] },
{ name: 'hotMeal', labelFa: 'ناهار و شام گرم', category: 'food', keywords: ['ناهار', 'شام', 'غذای گرم'] },
{ name: 'beverage', labelFa: 'نوشیدنی و چای‌خانه', category: 'food', keywords: ['چای', 'نوشیدنی', 'drink', 'قهوه'] },
{ name: 'halalFood', labelFa: 'غذای حلال ایرانی', category: 'food', keywords: ['حلال', 'halal', 'ایرانی'] },
{ name: 'snack', labelFa: 'میان‌وعده و پذیرایی', category: 'food', keywords: ['میان وعده', 'میوه', 'snack'] },
// ۴. زیارت و اماکن متبرکه
{ name: 'dome', labelFa: 'بارگاه و گنبد مقدس', category: 'ziyarat', keywords: ['گنبد', 'بارگاه', 'حرم', 'زیارت'] },
{ name: 'shrine', labelFa: 'حرم مطهر و ضریح', category: 'ziyarat', keywords: ['حرم', 'ضریح', 'زیارت', 'shrine'] },
{ name: 'kaaba', labelFa: 'کعبه مشرفه / حج و عمره', category: 'ziyarat', keywords: ['کعبه', 'مکه', 'حج', 'عمره', 'kaaba'] },
{ name: 'mosque', labelFa: 'مسجد و نماز جماعت', category: 'ziyarat', keywords: ['مسجد', 'نماز', 'mosque'] },
{ name: 'quran', labelFa: 'قرآن و ادعیه', category: 'ziyarat', keywords: ['قرآن', 'دعا', 'زیارت نامه', 'کتاب'] },
{ name: 'eulogy', labelFa: 'مداحی و مراسم مذهبی', category: 'ziyarat', keywords: ['مداح', 'نوحه', 'هیئت', 'مراسم'] },
{ name: 'holyTour', labelFa: 'زیارت دوره جامع', category: 'ziyarat', keywords: ['زیارت دوره', 'اماکن مقدسه', 'قبور شهدا'] },
// ۵. کادر، راهنما و لیدر
{ name: 'tourGuide', labelFa: 'مدیر کاروان و تورلیدر', category: 'staff', keywords: ['لیدر', 'راهنما', 'مدیر', 'guide'] },
{ name: 'scholar', labelFa: 'روحانی کاروان', category: 'staff', keywords: ['روحانی', 'شیخ', 'احکام', 'عالم'] },
{ name: 'translator', labelFa: 'مترجم بومی مسلط', category: 'staff', keywords: ['مترجم', 'عربی', 'انگلیسی', 'زبان'] },
{ name: 'doctor', labelFa: 'پزشک همراه کاروان', category: 'staff', keywords: ['پزشک', 'دکتر', 'درمان', 'کادر پزشکی'] },
{ name: 'medical', labelFa: 'خدمات امدادی و دارویی', category: 'staff', keywords: ['دارو', 'پزشکی', 'اورژانس'] },
{ name: 'camera', labelFa: 'عکاس و مستندساز', category: 'staff', keywords: ['عکاس', 'فیلمبردار', 'مستند'] },
{ name: 'support24', labelFa: 'پشتیبانی ۲۴ ساعته', category: 'staff', keywords: ['پشتیبانی', 'شبانه روزی', '۲۴ ساعته'] },
// ۶. مدارک، ویزا و امور مالی
{ name: 'passport', labelFa: 'گذرنامه و پاسپورت', category: 'finance', keywords: ['پاسپورت', 'گذرنامه', 'passport'] },
{ name: 'visa', labelFa: 'روادید و اخذ ویزا', category: 'finance', keywords: ['ویزا', 'روادید', 'visa'] },
{ name: 'insurance', labelFa: 'بیمه مسافرتی کامل', category: 'finance', keywords: ['بیمه', 'حوادث', 'درمان', 'insurance'] },
{ name: 'currency', labelFa: 'صرافی و ارز مسافرتی', category: 'finance', keywords: ['ارز', 'دلار', 'دینار', 'صرافی'] },
{ name: 'ticket', labelFa: 'بلیط ورودی و الکترونیک', category: 'finance', keywords: ['بلیط', 'بلیت', 'کارت', 'ticket'] },
{ name: 'tax', labelFa: 'عوارض خروج و مالیات', category: 'finance', keywords: ['عوارض', 'مالیات', 'فیش'] },
// ۷. ارتباطات و تجهیزات مسافر
{ name: 'wifi', labelFa: 'اینترنت وای‌فای رایگان', category: 'gear', keywords: ['وای فای', 'اینترنت', 'wifi'] },
{ name: 'simcard', labelFa: 'سیم‌کارت محلی فعال', category: 'gear', keywords: ['سیمکارت', 'شارژ', 'تماس', 'simcard'] },
{ name: 'luggage', labelFa: 'چمدان و ساک زیارتی', category: 'gear', keywords: ['چمدان', 'ساک', 'بار', 'luggage'] },
{ name: 'charger', labelFa: 'شارژر و ایستگاه برق', category: 'gear', keywords: ['شارژر', 'پاوربانک', 'پریز'] },
{ name: 'waterBottle', labelFa: 'آب معدنی بسته‌بندی', category: 'gear', keywords: ['آب', 'بطری', 'نوشیدنی'] },
{ name: 'umbrella', labelFa: 'چتر و سایبان', category: 'gear', keywords: ['چتر', 'سایبان', 'آفتاب'] },
// ۸. تفریح و خرید
{ name: 'shopping', labelFa: 'گشت بازار و خرید', category: 'leisure', keywords: ['بازار', 'خرید', 'مرکز خرید', 'shopping'] },
{ name: 'museum', labelFa: 'موزه و ابنیه تاریخی', category: 'leisure', keywords: ['موزه', 'تاریخی', 'آثار'] },
{ name: 'gift', labelFa: 'بسته هدایای تبرکی', category: 'leisure', keywords: ['هدیه', 'تبرکی', 'سوغات'] },
{ name: 'palmTour', labelFa: 'گشت نخلستان و طبیعت', category: 'leisure', keywords: ['نخلستان', 'طبیعت', 'گشت'] },
// شاخص و تیک پیش‌فرض
{ name: 'check', labelFa: 'تیک استاندارد', category: 'gear', keywords: ['تیک', 'شاخص', 'پیش فرض'] },
{ name: 'sparkles', labelFa: 'ویژه و ستاره‌دار', category: 'gear', keywords: ['ویژه', 'ستاره', 'جدید'] },
]
const CATEGORIES = [
{ id: 'all', label: 'همه' },
{ id: 'stay', label: 'اقامت' },
{ id: 'transport', label: 'ترانسفر' },
{ id: 'food', label: 'پذیرایی' },
{ id: 'ziyarat', label: 'زیارت' },
{ id: 'staff', label: 'کادر' },
{ id: 'finance', label: 'مدارک' },
{ id: 'gear', label: 'تجهیزات' },
{ id: 'leisure', label: 'گشت' },
] as const
interface TourFeatureIconPickerProps {
selectedIcon: string
onSelectIcon: (iconName: string) => void
disabled?: boolean
className?: string
buttonSize?: 'sm' | 'md'
size?: 'sm' | 'md'
}
export function TourFeatureIconPicker({
selectedIcon,
onSelectIcon,
disabled = false,
className,
buttonSize,
size,
}: TourFeatureIconPickerProps) {
const effectiveSize = size || buttonSize || 'md'
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const [activeCategory, setActiveCategory] = useState<string>('all')
const validSelected = selectedIcon && TOUR_FEATURE_ICONS.some((i) => i.name === selectedIcon)
? selectedIcon
: 'check'
const filteredIcons = useMemo(() => {
return TOUR_FEATURE_ICONS.filter((item) => {
const matchCategory = activeCategory === 'all' || item.category === activeCategory
const query = search.trim().toLowerCase()
const matchSearch =
!query ||
item.name.toLowerCase().includes(query) ||
item.labelFa.toLowerCase().includes(query) ||
item.keywords.some((k) => k.toLowerCase().includes(query))
return matchCategory && matchSearch
})
}, [search, activeCategory])
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<button
type="button"
disabled={disabled}
className={cn(
'flex items-center gap-1.5 rounded-xl border border-border-soft bg-surface-base hover:bg-card hover:border-primary/50 text-foreground transition-all cursor-pointer shrink-0 disabled:opacity-50',
effectiveSize === 'sm' ? 'h-7 px-2 text-xs' : 'h-9 px-2.5 text-xs',
className
)}
title="انتخاب آیکون ویژگی"
>
<div className="flex size-5 items-center justify-center rounded-lg bg-primary/10 text-primary">
<Ic name={validSelected as IconName} className="size-3.5" />
</div>
<Ic name="chevDown" className="size-3 text-grey-3" />
</button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-80 p-3 bg-card border-border-soft rounded-2xl shadow-xl z-50 space-y-2.5 backdrop-blur-xl"
>
{/* جستجوی آیکون */}
<div className="relative">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="جستجوی آیکون (هتل، اتوبوس، غذا، زیارت...)"
className="h-8 text-xs bg-surface-base pr-8 rounded-xl border-border-soft"
autoFocus
/>
<div className="absolute right-2.5 top-2 text-grey-3 pointer-events-none">
<Ic name="search" className="size-3.5" />
</div>
</div>
{/* دسته‌بندی‌ها */}
<div className="flex items-center gap-1 overflow-x-auto pb-1 no-scrollbar text-[11px]">
{CATEGORIES.map((cat) => (
<button
key={cat.id}
type="button"
onClick={() => setActiveCategory(cat.id)}
className={cn(
'px-2 py-0.5 rounded-lg whitespace-nowrap transition cursor-pointer font-medium',
activeCategory === cat.id
? 'bg-primary text-white font-bold'
: 'text-grey-3 hover:text-foreground hover:bg-surface-base'
)}
>
{cat.label}
</button>
))}
</div>
{/* شبکه آیکون‌ها */}
<div className="grid grid-cols-6 gap-1.5 max-h-56 overflow-y-auto p-1 border border-border-soft/60 rounded-xl bg-surface-base/50">
{filteredIcons.length === 0 ? (
<div className="col-span-6 py-6 text-center text-xs text-grey-3">
آیکونی یافت نشد
</div>
) : (
filteredIcons.map((item) => {
const isSelected = item.name === validSelected
return (
<button
key={item.name}
type="button"
onClick={() => {
onSelectIcon(item.name)
setOpen(false)
}}
title={item.labelFa}
className={cn(
'flex flex-col items-center justify-center p-2 rounded-xl border transition-all cursor-pointer group',
isSelected
? 'border-primary bg-primary/15 text-primary scale-105 shadow-xs'
: 'border-transparent hover:border-border-soft hover:bg-card text-grey-2 hover:text-foreground'
)}
>
<Ic
name={item.name}
className={cn(
'size-4 transition-transform group-hover:scale-110',
isSelected ? 'text-primary' : 'text-foreground'
)}
/>
</button>
)
})
)}
</div>
{/* راهنما */}
<div className="text-[10px] text-grey-3 text-center pt-0.5">
{filteredIcons.length} آیکون تخصصی گردشگری و زیارت
</div>
</PopoverContent>
</Popover>
)
}

274
src/features/tours/components/CreateTourDialog.tsx

@ -16,6 +16,7 @@ import {
ServiceTranslation, ServiceTranslation,
type ServiceLanguageCode, type ServiceLanguageCode,
} from '@/components/shared/ServiceTranslation' } from '@/components/shared/ServiceTranslation'
import { TourFeatureIconPicker } from '@/components/shared/TourFeatureIconPicker'
import type { TourTranslationData } from '../types' import type { TourTranslationData } from '../types'
interface CreateTourDialogProps { interface CreateTourDialogProps {
@ -70,8 +71,10 @@ export function CreateTourDialog({
const [percentOff, setPercentOff] = React.useState('0') const [percentOff, setPercentOff] = React.useState('0')
// Base features & tips // Base features & tips
const [features, setFeatures] = React.useState<string[]>([])
const [features, setFeatures] = React.useState<{ title: string; icon: string }[]>([])
const [newFeature, setNewFeature] = React.useState('') const [newFeature, setNewFeature] = React.useState('')
const [newFeatureIcon, setNewFeatureIcon] = React.useState('check')
const [editingBaseFeature, setEditingBaseFeature] = React.useState<{ idx: number; title: string; icon: string } | null>(null)
const [tips, setTips] = React.useState<{ title: string; desc: string }[]>([]) const [tips, setTips] = React.useState<{ title: string; desc: string }[]>([])
const [newTipTitle, setNewTipTitle] = React.useState('') const [newTipTitle, setNewTipTitle] = React.useState('')
const [newTipDesc, setNewTipDesc] = React.useState('') const [newTipDesc, setNewTipDesc] = React.useState('')
@ -95,6 +98,16 @@ export function CreateTourDialog({
setImageUrl(null) setImageUrl(null)
setImageName(null) setImageName(null)
setImageUploadError(null) setImageUploadError(null)
setFeatures([])
setNewFeature('')
setNewFeatureIcon('check')
setEditingBaseFeature(null)
setTips([])
setNewTipTitle('')
setNewTipDesc('')
setNewTransFeatText('')
setNewTransFeatIcon('check')
setEditingTransFeature(null)
} }
}, [open]) }, [open])
@ -140,11 +153,35 @@ export function CreateTourDialog({
const handleAddFeature = () => { const handleAddFeature = () => {
if (!newFeature.trim()) return if (!newFeature.trim()) return
setFeatures((prev) => [...prev, newFeature.trim()])
setFeatures((prev) => [...prev, { title: newFeature.trim(), icon: newFeatureIcon || 'check' }])
setNewFeature('') setNewFeature('')
setNewFeatureIcon('check')
}
const handleStartEditBaseFeature = (idx: number, feat: { title: string; icon: string }) => {
setEditingBaseFeature({ idx, title: feat.title, icon: feat.icon || 'check' })
}
const handleSaveEditBaseFeature = () => {
if (!editingBaseFeature || !editingBaseFeature.title.trim()) return
setFeatures((prev) =>
prev.map((f, i) =>
i === editingBaseFeature.idx
? { title: editingBaseFeature.title.trim(), icon: editingBaseFeature.icon || 'check' }
: f
)
)
setEditingBaseFeature(null)
}
const handleCancelEditBaseFeature = () => {
setEditingBaseFeature(null)
} }
const handleRemoveFeature = (idx: number) => { const handleRemoveFeature = (idx: number) => {
if (editingBaseFeature?.idx === idx) {
setEditingBaseFeature(null)
}
setFeatures((prev) => prev.filter((_, i) => i !== idx)) setFeatures((prev) => prev.filter((_, i) => i !== idx))
} }
@ -170,7 +207,7 @@ export function CreateTourDialog({
})) }))
} }
const handleAddTransFeature = (text: string) => {
const handleAddTransFeature = (text: string, icon = 'check') => {
if (!text.trim()) return if (!text.trim()) return
setTranslations((prev) => { setTranslations((prev) => {
const current = prev[selectedTransLang] || { tour_feature: [] } const current = prev[selectedTransLang] || { tour_feature: [] }
@ -179,13 +216,46 @@ export function CreateTourDialog({
...prev, ...prev,
[selectedTransLang]: { [selectedTransLang]: {
...current, ...current,
tour_feature: [...currFeats, { title: text.trim() }],
tour_feature: [...currFeats, { title: text.trim(), icon: icon || 'check' }],
},
}
})
setNewTransFeatText('')
setNewTransFeatIcon('check')
}
const handleStartEditTransFeature = (idx: number, feat: { title: string; icon?: string }) => {
setEditingTransFeature({ idx, title: feat.title, icon: feat.icon || 'check' })
}
const handleSaveEditTransFeature = () => {
if (!editingTransFeature || !editingTransFeature.title.trim()) return
setTranslations((prev) => {
const current = prev[selectedTransLang] || { tour_feature: [] }
const currFeats = current.tour_feature || []
return {
...prev,
[selectedTransLang]: {
...current,
tour_feature: currFeats.map((f, i) =>
i === editingTransFeature.idx
? { title: editingTransFeature.title.trim(), icon: editingTransFeature.icon || 'check' }
: f
),
}, },
} }
}) })
setEditingTransFeature(null)
}
const handleCancelEditTransFeature = () => {
setEditingTransFeature(null)
} }
const handleRemoveTransFeature = (idx: number) => { const handleRemoveTransFeature = (idx: number) => {
if (editingTransFeature?.idx === idx) {
setEditingTransFeature(null)
}
setTranslations((prev) => { setTranslations((prev) => {
const current = prev[selectedTransLang] || { tour_feature: [] } const current = prev[selectedTransLang] || { tour_feature: [] }
const currFeats = current.tour_feature || [] const currFeats = current.tour_feature || []
@ -233,6 +303,8 @@ export function CreateTourDialog({
} }
const [newTransFeatText, setNewTransFeatText] = React.useState('') const [newTransFeatText, setNewTransFeatText] = React.useState('')
const [newTransFeatIcon, setNewTransFeatIcon] = React.useState('check')
const [editingTransFeature, setEditingTransFeature] = React.useState<{ idx: number; title: string; icon: string } | null>(null)
const [newTransTipTitle, setNewTransTipTitle] = React.useState('') const [newTransTipTitle, setNewTransTipTitle] = React.useState('')
const [newTransTipDesc, setNewTransTipDesc] = React.useState('') const [newTransTipDesc, setNewTransTipDesc] = React.useState('')
@ -281,7 +353,7 @@ export function CreateTourDialog({
setIsSubmitting(true) setIsSubmitting(true)
try { try {
const tour_feature = features.map((f) => ({ title: f }))
const tour_feature = features.map((f) => ({ title: f.title, icon: f.icon || 'check' }))
const travel_tips: Record<string, string> = {} const travel_tips: Record<string, string> = {}
tips.forEach((t) => { tips.forEach((t) => {
travel_tips[t.title] = t.desc travel_tips[t.title] = t.desc
@ -780,6 +852,10 @@ export function CreateTourDialog({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TourFeatureIconPicker
selectedIcon={newFeatureIcon}
onSelectIcon={setNewFeatureIcon}
/>
<Input <Input
value={newFeature} value={newFeature}
onChange={(e) => setNewFeature(e.target.value)} onChange={(e) => setNewFeature(e.target.value)}
@ -796,7 +872,7 @@ export function CreateTourDialog({
type="button" type="button"
size="sm" size="sm"
onClick={handleAddFeature} onClick={handleAddFeature}
className="shrink-0 bg-primary/20 text-primary hover:bg-primary hover:text-white text-xs h-9 rounded-xl"
className="shrink-0 bg-primary/20 text-primary hover:bg-primary hover:text-white text-xs h-9 rounded-xl cursor-pointer"
> >
<Ic name="plus" className="size-4" /> <Ic name="plus" className="size-4" />
<span>افزودن</span> <span>افزودن</span>
@ -809,21 +885,80 @@ export function CreateTourDialog({
هنوز ویژگی برای این تور اضافه نشده است. هنوز ویژگی برای این تور اضافه نشده است.
</p> </p>
) : ( ) : (
features.map((feat, idx) => (
<div
key={idx}
className="flex items-center justify-between gap-2 rounded-xl bg-surface-2 border border-border/40 px-3 py-2 text-xs"
>
<span className="font-medium text-foreground truncate">{feat}</span>
<button
type="button"
onClick={() => handleRemoveFeature(idx)}
className="text-grey-4 hover:text-rose-400 p-1 shrink-0"
features.map((feat, idx) =>
editingBaseFeature?.idx === idx ? (
<div
key={idx}
className="flex items-center gap-1.5 rounded-xl border border-primary/50 bg-card p-1 text-xs shadow-xs col-span-1 sm:col-span-2"
> >
<Ic name="x" className="size-3.5" />
</button>
</div>
))
<TourFeatureIconPicker
selectedIcon={editingBaseFeature.icon}
onSelectIcon={(icon) => setEditingBaseFeature({ ...editingBaseFeature, icon })}
size="sm"
/>
<Input
value={editingBaseFeature.title}
onChange={(e) => setEditingBaseFeature({ ...editingBaseFeature, title: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSaveEditBaseFeature()
} else if (e.key === 'Escape') {
handleCancelEditBaseFeature()
}
}}
autoFocus
className="h-8 bg-surface-base text-xs rounded-lg px-2 border-border-soft flex-1"
/>
<button
type="button"
onClick={handleSaveEditBaseFeature}
className="flex size-7 items-center justify-center rounded-lg bg-primary text-white hover:bg-primary/90 cursor-pointer transition shrink-0"
title="ثبت ویرایش"
>
<Ic name="check" className="size-3.5" />
</button>
<button
type="button"
onClick={handleCancelEditBaseFeature}
className="flex size-7 items-center justify-center rounded-lg text-grey-3 hover:text-foreground cursor-pointer transition shrink-0"
title="انصراف"
>
<Ic name="x" className="size-3.5" />
</button>
</div>
) : (
<div
key={idx}
className="flex items-center justify-between gap-2 rounded-xl bg-surface-2 border border-border/40 px-3 py-2 text-xs"
>
<div className="flex items-center gap-2 truncate">
<span className="flex size-6 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<Ic name={(feat.icon || 'check') as any} className="size-3.5" />
</span>
<span className="font-medium text-foreground truncate">{feat.title}</span>
</div>
<div className="flex items-center gap-1 shrink-0 text-grey-4">
<button
type="button"
onClick={() => handleStartEditBaseFeature(idx, feat)}
className="p-1 hover:text-primary transition rounded-md cursor-pointer"
title="ویرایش"
>
<Ic name="edit" className="size-3.5" />
</button>
<button
type="button"
onClick={() => handleRemoveFeature(idx)}
className="p-1 hover:text-rose-400 transition rounded-md cursor-pointer"
title="حذف"
>
<Ic name="trash" className="size-3.5" />
</button>
</div>
</div>
)
)
)} )}
</div> </div>
</div> </div>
@ -944,6 +1079,10 @@ export function CreateTourDialog({
ویژگی‌ها به زبان {activeTransConfig?.label} ({(currentTransData.tour_feature || []).length}): ویژگی‌ها به زبان {activeTransConfig?.label} ({(currentTransData.tour_feature || []).length}):
</label> </label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TourFeatureIconPicker
selectedIcon={newTransFeatIcon}
onSelectIcon={setNewTransFeatIcon}
/>
<Input <Input
value={newTransFeatText} value={newTransFeatText}
onChange={(e) => setNewTransFeatText(e.target.value)} onChange={(e) => setNewTransFeatText(e.target.value)}
@ -952,8 +1091,7 @@ export function CreateTourDialog({
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
e.preventDefault() e.preventDefault()
handleAddTransFeature(newTransFeatText)
setNewTransFeatText('')
handleAddTransFeature(newTransFeatText, newTransFeatIcon)
} }
}} }}
/> />
@ -961,10 +1099,9 @@ export function CreateTourDialog({
type="button" type="button"
size="sm" size="sm"
onClick={() => { onClick={() => {
handleAddTransFeature(newTransFeatText)
setNewTransFeatText('')
handleAddTransFeature(newTransFeatText, newTransFeatIcon)
}} }}
className="shrink-0 bg-primary/20 text-primary hover:bg-primary hover:text-white text-xs h-9 rounded-xl"
className="shrink-0 bg-primary/20 text-primary hover:bg-primary hover:text-white text-xs h-9 rounded-xl cursor-pointer"
> >
<Ic name="plus" className="size-4" /> <Ic name="plus" className="size-4" />
<span>افزودن</span> <span>افزودن</span>
@ -972,21 +1109,80 @@ export function CreateTourDialog({
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
{(currentTransData.tour_feature || []).map((feat, fIdx) => (
<div
key={fIdx}
className="flex items-center justify-between rounded-xl bg-surface-2 border border-border/40 px-3 py-1.5 text-xs"
>
<span className="font-medium text-foreground">{feat.title}</span>
<button
type="button"
onClick={() => handleRemoveTransFeature(fIdx)}
className="text-grey-4 hover:text-rose-400 p-0.5"
{(currentTransData.tour_feature || []).map((feat, fIdx) =>
editingTransFeature?.idx === fIdx ? (
<div
key={fIdx}
className="flex items-center gap-1.5 rounded-xl border border-primary/50 bg-card p-1 text-xs shadow-xs"
> >
<Ic name="x" className="size-3.5" />
</button>
</div>
))}
<TourFeatureIconPicker
selectedIcon={editingTransFeature.icon}
onSelectIcon={(icon) => setEditingTransFeature({ ...editingTransFeature, icon })}
size="sm"
/>
<Input
value={editingTransFeature.title}
onChange={(e) => setEditingTransFeature({ ...editingTransFeature, title: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSaveEditTransFeature()
} else if (e.key === 'Escape') {
handleCancelEditTransFeature()
}
}}
autoFocus
className="h-8 bg-surface-base text-xs rounded-lg px-2 border-border-soft flex-1"
/>
<button
type="button"
onClick={handleSaveEditTransFeature}
className="flex size-7 items-center justify-center rounded-lg bg-primary text-white hover:bg-primary/90 cursor-pointer transition shrink-0"
title="ثبت ویرایش"
>
<Ic name="check" className="size-3.5" />
</button>
<button
type="button"
onClick={handleCancelEditTransFeature}
className="flex size-7 items-center justify-center rounded-lg text-grey-3 hover:text-foreground cursor-pointer transition shrink-0"
title="انصراف"
>
<Ic name="x" className="size-3.5" />
</button>
</div>
) : (
<div
key={fIdx}
className="flex items-center justify-between rounded-xl bg-surface-2 border border-border/40 px-3 py-1.5 text-xs"
>
<div className="flex items-center gap-2 truncate">
<span className="flex size-5 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<Ic name={(feat.icon || 'check') as any} className="size-3" />
</span>
<span className="font-medium text-foreground truncate">{feat.title}</span>
</div>
<div className="flex items-center gap-1 shrink-0 text-grey-4">
<button
type="button"
onClick={() => handleStartEditTransFeature(fIdx, feat)}
className="p-1 hover:text-primary transition rounded-md cursor-pointer"
title="ویرایش"
>
<Ic name="edit" className="size-3.5" />
</button>
<button
type="button"
onClick={() => handleRemoveTransFeature(fIdx)}
className="p-1 hover:text-rose-400 transition rounded-md cursor-pointer"
title="حذف"
>
<Ic name="trash" className="size-3.5" />
</button>
</div>
</div>
)
)}
</div> </div>
</div> </div>

381
src/features/tours/components/TourDetailDialog.tsx

@ -50,6 +50,7 @@ import type {
} from '../types' } from '../types'
import { TourStatusBadge } from './TourStatusBadge' import { TourStatusBadge } from './TourStatusBadge'
import DeleteConfirmDialog from '@/components/shared/DeleteConfirmDialog' import DeleteConfirmDialog from '@/components/shared/DeleteConfirmDialog'
import { TourFeatureIconPicker } from '@/components/shared/TourFeatureIconPicker'
interface TourDetailDialogProps { interface TourDetailDialogProps {
tourId: number | null tourId: number | null
@ -68,10 +69,15 @@ const SUPPORTED_LANGUAGES = [
type LangCode = (typeof SUPPORTED_LANGUAGES)[number]['code'] type LangCode = (typeof SUPPORTED_LANGUAGES)[number]['code']
export interface FeatureDraftItem {
title: string
icon?: string
}
interface LangDraft { interface LangDraft {
title: string title: string
description: string description: string
features: string[]
features: FeatureDraftItem[]
tips: { title: string; desc: string }[] tips: { title: string; desc: string }[]
} }
@ -173,9 +179,14 @@ export function TourDetailDialog({
// Temporary local inputs for adding feature / tip // Temporary local inputs for adding feature / tip
const [newFeatureInput, setNewFeatureInput] = useState<string>('') const [newFeatureInput, setNewFeatureInput] = useState<string>('')
const [newFeatureIcon, setNewFeatureIcon] = useState<string>('check')
const [newTipTitle, setNewTipTitle] = useState<string>('') const [newTipTitle, setNewTipTitle] = useState<string>('')
const [newTipDesc, setNewTipDesc] = useState<string>('') const [newTipDesc, setNewTipDesc] = useState<string>('')
// State for inline editing an existing travel tip & feature
const [editingTip, setEditingTip] = useState<{ idx: number; title: string; desc: string } | null>(null)
const [editingFeature, setEditingFeature] = useState<{ idx: number; title: string; icon: string } | null>(null)
// Add new itinerary step state // Add new itinerary step state
const [showAddItinForm, setShowAddItinForm] = useState<boolean>(false) const [showAddItinForm, setShowAddItinForm] = useState<boolean>(false)
const [newItinTitle, setNewItinTitle] = useState<string>('') const [newItinTitle, setNewItinTitle] = useState<string>('')
@ -213,6 +224,8 @@ export function TourDetailDialog({
setDeletedTourImageIds([]) setDeletedTourImageIds([])
setNewFilerImageIds([]) setNewFilerImageIds([])
setImageUploadError(null) setImageUploadError(null)
setEditingTip(null)
setEditingFeature(null)
return return
} }
@ -223,6 +236,8 @@ export function TourDetailDialog({
setActiveTab('overview') setActiveTab('overview')
setIsEditMode(false) setIsEditMode(false)
setShowAddItinForm(false) setShowAddItinForm(false)
setEditingTip(null)
setEditingFeature(null)
Promise.all([ Promise.all([
fetchTourDetail(tourId), fetchTourDetail(tourId),
@ -281,7 +296,11 @@ export function TourDetailDialog({
initialDrafts[lang.code] = { initialDrafts[lang.code] = {
title: langTrans.title ?? '', title: langTrans.title ?? '',
description: langTrans.description ?? '', description: langTrans.description ?? '',
features: (langTrans.tour_feature || []).map((f: any) => (typeof f === 'string' ? f : f.title)),
features: (langTrans.tour_feature || []).map((f: any) =>
typeof f === 'string'
? { title: f, icon: 'check' }
: { title: f?.title || '', icon: f?.icon || 'check' }
),
tips: Object.entries(langTrans.travel_tips || {}).map(([tTitle, tDesc]) => ({ tips: Object.entries(langTrans.travel_tips || {}).map(([tTitle, tDesc]) => ({
title: tTitle, title: tTitle,
desc: String(tDesc), desc: String(tDesc),
@ -291,7 +310,11 @@ export function TourDetailDialog({
initialDrafts[lang.code] = { initialDrafts[lang.code] = {
title: trans.primary_data.title ?? '', title: trans.primary_data.title ?? '',
description: trans.primary_data.description ?? '', description: trans.primary_data.description ?? '',
features: (trans.primary_data.tour_feature || []).map((f: any) => (typeof f === 'string' ? f : f.title)),
features: (trans.primary_data.tour_feature || []).map((f: any) =>
typeof f === 'string'
? { title: f, icon: 'check' }
: { title: f?.title || '', icon: f?.icon || 'check' }
),
tips: Object.entries(trans.primary_data.travel_tips || {}).map(([tTitle, tDesc]) => ({ tips: Object.entries(trans.primary_data.travel_tips || {}).map(([tTitle, tDesc]) => ({
title: tTitle, title: tTitle,
desc: String(tDesc), desc: String(tDesc),
@ -301,7 +324,11 @@ export function TourDetailDialog({
initialDrafts[lang.code] = { initialDrafts[lang.code] = {
title: detail.title ?? '', title: detail.title ?? '',
description: detail.description ?? '', description: detail.description ?? '',
features: (detail.tour_features || []).map((f) => f.title),
features: (detail.tour_features || []).map((f: any) =>
typeof f === 'string'
? { title: f, icon: 'check' }
: { title: f?.title || '', icon: f?.icon || 'check' }
),
tips: (detail.travel_tips || []).map((t) => ({ tips: (detail.travel_tips || []).map((t) => ({
title: t.title, title: t.title,
desc: t.description, desc: t.description,
@ -346,6 +373,12 @@ export function TourDetailDialog({
} }
}, [tourId, isOpen]) }, [tourId, isOpen])
// Reset inline edit states when language, tab or edit mode changes
useEffect(() => {
setEditingTip(null)
setEditingFeature(null)
}, [selectedLang, activeTab, isEditMode])
// Load sub-tabs (passengers, comments, itineraries) on demand // Load sub-tabs (passengers, comments, itineraries) on demand
useEffect(() => { useEffect(() => {
if (!tourId || !isOpen || !tour) return if (!tourId || !isOpen || !tour) return
@ -493,13 +526,44 @@ export function TourDetailDialog({
...prev, ...prev,
[selectedLang]: { [selectedLang]: {
...prev[selectedLang], ...prev[selectedLang],
features: [...prev[selectedLang].features, newFeatureInput.trim()],
features: [
...prev[selectedLang].features,
{ title: newFeatureInput.trim(), icon: newFeatureIcon || 'check' },
],
}, },
})) }))
setNewFeatureInput('') setNewFeatureInput('')
setNewFeatureIcon('check')
}
const handleStartEditFeature = (idx: number, feat: FeatureDraftItem) => {
setEditingFeature({ idx, title: feat.title, icon: feat.icon || 'check' })
}
const handleSaveEditFeature = () => {
if (!editingFeature || !editingFeature.title.trim()) return
setAllTranslations((prev) => ({
...prev,
[selectedLang]: {
...prev[selectedLang],
features: prev[selectedLang].features.map((f, i) =>
i === editingFeature.idx
? { title: editingFeature.title.trim(), icon: editingFeature.icon || 'check' }
: f
),
},
}))
setEditingFeature(null)
}
const handleCancelEditFeature = () => {
setEditingFeature(null)
} }
const handleRemoveFeature = (idx: number) => { const handleRemoveFeature = (idx: number) => {
if (editingFeature?.idx === idx) {
setEditingFeature(null)
}
setAllTranslations((prev) => ({ setAllTranslations((prev) => ({
...prev, ...prev,
[selectedLang]: { [selectedLang]: {
@ -522,7 +586,34 @@ export function TourDetailDialog({
setNewTipDesc('') setNewTipDesc('')
} }
const handleStartEditTip = (idx: number, tip: { title: string; desc: string }) => {
setEditingTip({ idx, title: tip.title, desc: tip.desc })
}
const handleSaveEditTip = () => {
if (!editingTip || !editingTip.title.trim()) return
setAllTranslations((prev) => ({
...prev,
[selectedLang]: {
...prev[selectedLang],
tips: prev[selectedLang].tips.map((t, i) =>
i === editingTip.idx
? { title: editingTip.title.trim(), desc: editingTip.desc.trim() }
: t
),
},
}))
setEditingTip(null)
}
const handleCancelEditTip = () => {
setEditingTip(null)
}
const handleRemoveTip = (idx: number) => { const handleRemoveTip = (idx: number) => {
if (editingTip?.idx === idx) {
setEditingTip(null)
}
setAllTranslations((prev) => ({ setAllTranslations((prev) => ({
...prev, ...prev,
[selectedLang]: { [selectedLang]: {
@ -762,7 +853,9 @@ export function TourDetailDialog({
translationsPayload[lang.code] = { translationsPayload[lang.code] = {
title: draft.title.trim(), title: draft.title.trim(),
description: draft.description.trim(), description: draft.description.trim(),
tour_feature: draft.features.map((f) => ({ title: f.trim() })).filter((f) => f.title),
tour_feature: draft.features
.map((f) => ({ title: f.title.trim(), icon: f.icon || 'check' }))
.filter((f) => f.title),
travel_tips: travel_tips_obj, travel_tips: travel_tips_obj,
} }
} }
@ -1067,8 +1160,39 @@ export function TourDetailDialog({
</div> </div>
</div> </div>
{/* Integrated 5-Language Selector Bar with dirty indicators via ServiceTranslation */}
<div className="mt-4 pt-2 border-t border-border-soft/60">
{/* Unified Navigation Tabs and Language Bar on a single line */}
<div className="mt-3 pt-2.5 border-t border-border-soft/60 flex items-center justify-between gap-2 overflow-x-auto no-scrollbar flex-nowrap">
{/* Main Navigation Tabs */}
<Tabs
value={activeTab}
onValueChange={(val: any) => setActiveTab(val)}
className="shrink-0"
>
<TabsList className="flex items-center h-8.5 overflow-x-auto no-scrollbar bg-surface-base border border-border-soft p-0.5 rounded-xl shrink-0">
<TabsTrigger value="overview" className="h-7.5 text-xs gap-1.5 shrink-0 px-2.5 rounded-lg">
<Ic name="file" className="size-3.5 shrink-0" />
<span>مشخصات و متون</span>
</TabsTrigger>
<TabsTrigger value="itinerary" className="h-7.5 text-xs gap-1.5 shrink-0 px-2.5 rounded-lg">
<Ic name="calendar" className="size-3.5 shrink-0" />
<span>برنامه سفر</span>
</TabsTrigger>
<TabsTrigger value="features" className="h-7.5 text-xs gap-1.5 shrink-0 px-2.5 rounded-lg">
<Ic name="star" className="size-3.5 shrink-0" />
<span>امکانات و نکات</span>
</TabsTrigger>
<TabsTrigger value="passengers" className="h-7.5 text-xs gap-1.5 shrink-0 px-2.5 rounded-lg">
<Ic name="users" className="size-3.5 shrink-0" />
<span>مسافران</span>
</TabsTrigger>
<TabsTrigger value="comments" className="h-7.5 text-xs gap-1.5 shrink-0 px-2.5 rounded-lg">
<Ic name="message" className="size-3.5 shrink-0" />
<span>نظرات</span>
</TabsTrigger>
</TabsList>
</Tabs>
{/* Integrated 5-Language Selector Bar with dirty indicators via ServiceTranslation */}
<ServiceTranslation <ServiceTranslation
activeLang={selectedLang as ServiceLanguageCode} activeLang={selectedLang as ServiceLanguageCode}
onLangChange={(lang) => handleLanguageSwitch(lang)} onLangChange={(lang) => handleLanguageSwitch(lang)}
@ -1090,40 +1214,12 @@ export function TourDetailDialog({
]) ])
)} )}
primaryLang={transData?.primary_language as ServiceLanguageCode} primaryLang={transData?.primary_language as ServiceLanguageCode}
title="زبان فعال محتوا"
variant="pills"
title=""
showGlobeIcon={true}
variant="inline"
className="shrink-0"
/> />
</div> </div>
{/* Main Navigation Tabs */}
<Tabs
value={activeTab}
onValueChange={(val: any) => setActiveTab(val)}
className="mt-3 w-full"
>
<TabsList className="flex w-full overflow-x-auto no-scrollbar sm:grid sm:grid-cols-5 bg-surface-base border border-border-soft p-1">
<TabsTrigger value="overview" className="text-xs gap-1.5 shrink-0">
<Ic name="file" className="size-3.5 shrink-0" />
<span>مشخصات و متون</span>
</TabsTrigger>
<TabsTrigger value="itinerary" className="text-xs gap-1.5 shrink-0">
<Ic name="calendar" className="size-3.5 shrink-0" />
<span>برنامه سفر</span>
</TabsTrigger>
<TabsTrigger value="features" className="text-xs gap-1.5 shrink-0">
<Ic name="star" className="size-3.5 shrink-0" />
<span>امکانات و نکات</span>
</TabsTrigger>
<TabsTrigger value="passengers" className="text-xs gap-1.5 shrink-0">
<Ic name="users" className="size-3.5 shrink-0" />
<span>مسافران</span>
</TabsTrigger>
<TabsTrigger value="comments" className="text-xs gap-1.5 shrink-0">
<Ic name="message" className="size-3.5 shrink-0" />
<span>نظرات</span>
</TabsTrigger>
</TabsList>
</Tabs>
</DialogHeader> </DialogHeader>
{/* Modal Scrollable Body */} {/* Modal Scrollable Body */}
@ -1758,29 +1854,91 @@ export function TourDetailDialog({
{currentLangDraft.features.length === 0 ? ( {currentLangDraft.features.length === 0 ? (
<span className="text-xs text-grey-3">هیچ امکانی ثبت نشده است.</span> <span className="text-xs text-grey-3">هیچ امکانی ثبت نشده است.</span>
) : ( ) : (
currentLangDraft.features.map((feat, idx) => (
<div
key={idx}
className="flex items-center gap-1.5 rounded-xl border border-border-soft bg-surface-base px-3 py-1.5 text-xs font-bold text-foreground"
dir={activeLangConfig.dir}
>
<span>{feat}</span>
{isEditMode && (
currentLangDraft.features.map((feat, idx) =>
editingFeature?.idx === idx ? (
<div
key={idx}
className="flex items-center gap-1.5 rounded-xl border border-primary/50 bg-card p-1 text-xs shadow-xs"
dir={activeLangConfig.dir}
>
<TourFeatureIconPicker
selectedIcon={editingFeature.icon}
onSelectIcon={(icon) => setEditingFeature({ ...editingFeature, icon })}
size="sm"
/>
<Input
value={editingFeature.title}
onChange={(e) => setEditingFeature({ ...editingFeature, title: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSaveEditFeature()
} else if (e.key === 'Escape') {
handleCancelEditFeature()
}
}}
autoFocus
className="h-7 w-36 bg-surface-base text-xs rounded-lg px-2 border-border-soft"
/>
<button <button
type="button" type="button"
onClick={() => handleRemoveFeature(idx)}
className="text-grey-3 hover:text-rose-500 cursor-pointer"
onClick={handleSaveEditFeature}
className="flex size-6 items-center justify-center rounded-md bg-primary text-white hover:bg-primary/90 cursor-pointer transition"
title="ثبت ویرایش"
> >
&times;
<Ic name="check" className="size-3" />
</button> </button>
)}
</div>
))
<button
type="button"
onClick={handleCancelEditFeature}
className="flex size-6 items-center justify-center rounded-md text-grey-3 hover:text-foreground cursor-pointer transition"
title="انصراف"
>
<Ic name="x" className="size-3" />
</button>
</div>
) : (
<div
key={idx}
className="flex items-center gap-2 rounded-xl border border-border-soft bg-surface-base px-2.5 py-1.5 text-xs font-bold text-foreground transition hover:border-border-soft/80"
dir={activeLangConfig.dir}
>
<span className="flex size-5 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<Ic name={(feat.icon || 'check') as any} className="size-3.5" />
</span>
<span>{feat.title}</span>
{isEditMode && (
<div className="flex items-center gap-1 mr-1 border-r border-border-soft pr-1.5 text-grey-3">
<button
type="button"
onClick={() => handleStartEditFeature(idx, feat)}
className="p-0.5 hover:text-primary cursor-pointer transition rounded-sm"
title="ویرایش امکان"
>
<Ic name="edit" className="size-3" />
</button>
<button
type="button"
onClick={() => handleRemoveFeature(idx)}
className="p-0.5 hover:text-rose-500 cursor-pointer transition rounded-sm"
title="حذف امکان"
>
<Ic name="trash" className="size-3" />
</button>
</div>
)}
</div>
)
)
)} )}
</div> </div>
{isEditMode && ( {isEditMode && (
<div className="flex items-center gap-2 pt-2"> <div className="flex items-center gap-2 pt-2">
<TourFeatureIconPicker
selectedIcon={newFeatureIcon}
onSelectIcon={setNewFeatureIcon}
/>
<Input <Input
value={newFeatureInput} value={newFeatureInput}
onChange={(e) => setNewFeatureInput(e.target.value)} onChange={(e) => setNewFeatureInput(e.target.value)}
@ -1815,27 +1973,102 @@ export function TourDetailDialog({
{currentLangDraft.tips.length === 0 ? ( {currentLangDraft.tips.length === 0 ? (
<span className="text-xs text-grey-3">هیچ نکته‌ای ثبت نشده است.</span> <span className="text-xs text-grey-3">هیچ نکته‌ای ثبت نشده است.</span>
) : ( ) : (
currentLangDraft.tips.map((tip, idx) => (
<div
key={idx}
className="rounded-xl border border-border-soft bg-surface-base p-3 space-y-1"
dir={activeLangConfig.dir}
>
<div className="flex items-center justify-between">
<span className="font-bold text-xs text-foreground">{tip.title}</span>
{isEditMode && (
<button
type="button"
onClick={() => handleRemoveTip(idx)}
className="text-grey-3 hover:text-rose-500 cursor-pointer text-xs"
>
حذف
</button>
)}
currentLangDraft.tips.map((tip, idx) =>
editingTip?.idx === idx ? (
<div
key={idx}
className="rounded-xl border border-primary/50 bg-card p-3 space-y-2.5 shadow-xs"
dir={activeLangConfig.dir}
>
<div className="flex items-center justify-between gap-2">
<span className="text-[11px] font-bold text-primary">ویرایش نکته سفر:</span>
<div className="flex items-center gap-1.5">
<Button
type="button"
size="sm"
onClick={handleSaveEditTip}
className="h-7 px-2.5 text-xs font-bold gap-1 bg-primary text-white hover:bg-primary/90 rounded-lg cursor-pointer"
title="ثبت ویرایش"
>
<Ic name="check" className="size-3.5" />
<span>ثبت</span>
</Button>
<Button
type="button"
size="sm"
variant="ghost"
onClick={handleCancelEditTip}
className="h-7 px-2 text-xs text-grey-3 hover:text-foreground rounded-lg cursor-pointer"
title="انصراف"
>
<Ic name="x" className="size-3.5" />
<span>انصراف</span>
</Button>
</div>
</div>
<Input
value={editingTip.title}
onChange={(e) => setEditingTip({ ...editingTip, title: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault()
handleSaveEditTip()
} else if (e.key === 'Escape') {
handleCancelEditTip()
}
}}
placeholder={`عنوان نکته به زبان ${activeLangConfig.label}...`}
dir={activeLangConfig.dir}
className="bg-surface-base text-xs rounded-xl border-border-soft h-8"
autoFocus
/>
<textarea
rows={2}
value={editingTip.desc}
onChange={(e) => setEditingTip({ ...editingTip, desc: e.target.value })}
onKeyDown={(e) => {
if (e.key === 'Escape') {
handleCancelEditTip()
}
}}
placeholder={`شرح راهنما به زبان ${activeLangConfig.label}...`}
dir={activeLangConfig.dir}
className="w-full rounded-xl border border-border-soft bg-surface-base p-2 text-xs text-foreground focus:outline-hidden"
/>
</div> </div>
<p className="text-xs text-grey-2 leading-relaxed">{tip.desc}</p>
</div>
))
) : (
<div
key={idx}
className="group rounded-xl border border-border-soft bg-surface-base p-3 space-y-1 transition hover:border-border-soft/80"
dir={activeLangConfig.dir}
>
<div className="flex items-center justify-between gap-2">
<span className="font-bold text-xs text-foreground">{tip.title}</span>
{isEditMode && (
<div className="flex items-center gap-1 shrink-0">
<button
type="button"
onClick={() => handleStartEditTip(idx, tip)}
className="flex size-7 items-center justify-center rounded-lg border border-border-soft bg-card/60 text-grey-3 hover:border-primary/40 hover:bg-primary/10 hover:text-primary transition cursor-pointer"
title="ویرایش نکته"
>
<Ic name="edit" className="size-3.5" />
</button>
<button
type="button"
onClick={() => handleRemoveTip(idx)}
className="flex size-7 items-center justify-center rounded-lg border border-border-soft bg-card/60 text-grey-3 hover:border-rose-500/40 hover:bg-rose-500/10 hover:text-rose-500 transition cursor-pointer"
title="حذف نکته"
>
<Ic name="trash" className="size-3.5" />
</button>
</div>
)}
</div>
<p className="text-xs text-grey-2 leading-relaxed">{tip.desc}</p>
</div>
)
)
)} )}
</div> </div>

4
src/features/tours/services/tour-api.ts

@ -107,7 +107,7 @@ export async function saveTourTranslations(
lang_code?: string lang_code?: string
title?: string title?: string
description?: string description?: string
tour_feature?: { title: string }[]
tour_feature?: { title: string; icon?: string }[]
travel_tips?: Record<string, string> travel_tips?: Record<string, string>
translations?: Record<string, import('../types').TourTranslationData> translations?: Record<string, import('../types').TourTranslationData>
} }
@ -209,7 +209,7 @@ export interface CreateTourPayload {
language_code?: string language_code?: string
status?: string status?: string
trip_status?: string trip_status?: string
tour_feature?: { title: string }[]
tour_feature?: { title: string; icon?: string }[]
travel_tips?: Record<string, string> travel_tips?: Record<string, string>
visibility?: boolean visibility?: boolean
translations?: Record<string, import('../types').TourTranslationData> translations?: Record<string, import('../types').TourTranslationData>

5
src/features/tours/types.ts

@ -45,6 +45,7 @@ export interface TourListItem {
export interface TourFeatureItem { export interface TourFeatureItem {
id: number id: number
title: string title: string
icon?: string
} }
export interface TravelTipItem { export interface TravelTipItem {
@ -159,7 +160,7 @@ export interface TourTranslationData {
id?: number id?: number
title: string title: string
description?: string description?: string
tour_feature?: { title: string }[]
tour_feature?: { title: string; icon?: string }[]
travel_tips?: Record<string, string> travel_tips?: Record<string, string>
} }
@ -168,7 +169,7 @@ export interface TourTranslationsResponse {
primary_data: { primary_data: {
title: string title: string
description: string description: string
tour_feature: { title: string }[]
tour_feature: { title: string; icon?: string }[]
travel_tips: Record<string, string> travel_tips: Record<string, string>
} }
translations: Record<string, TourTranslationData> translations: Record<string, TourTranslationData>

68
src/icons.tsx

@ -126,6 +126,74 @@ const PATHS: Record<string, string> = {
linkedin: '<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect width="4" height="12" x="2" y="9"/><circle cx="4" cy="4" r="2"/>', linkedin: '<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"/><rect width="4" height="12" x="2" y="9"/><circle cx="4" cy="4" r="2"/>',
tiktok: '<path d="M9 12a4 4 0 1 0 4 4V4a5 5 0 0 0 5 5"/>', tiktok: '<path d="M9 12a4 4 0 1 0 4 4V4a5 5 0 0 0 5 5"/>',
share2: '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" x2="15.42" y1="13.51" y2="17.49"/><line x1="15.41" x2="8.59" y1="6.51" y2="10.49"/>', share2: '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" x2="15.42" y1="13.51" y2="17.49"/><line x1="15.41" x2="8.59" y1="6.51" y2="10.49"/>',
info: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>',
/* ────────── مجموعه آیکون‌های اختصاصی خدمات تور و گردشگری زیارتی (Tour & Travel Features) ────────── */
// ۱. حمل‌ونقل و ترانسفر
busVip: '<path d="M4 6a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6Z"/><path d="M4 11h16"/><path d="M9 18v2"/><path d="M15 18v2"/><circle cx="7.5" cy="14.5" r="1.5"/><circle cx="16.5" cy="14.5" r="1.5"/>',
van: '<path d="M3 6a2 2 0 0 1 2-2h10l4 4v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V6Z"/><circle cx="7" cy="17" r="2"/><circle cx="16" cy="17" r="2"/><path d="M3 12h16"/><path d="M15 4v4h4"/>',
airplane: '<path d="M17.8 19.2 16 11l3.5-3.5C21 6 21.5 4 20 2.5c-1.5-1.5-3.5-1-5 0.5L11.5 6.5 3.3 4.7c-.5-.1-.9.1-1.1.5l-.7 1.4c-.2.4 0 .9.3 1.1l6.2 4.2-3.5 3.5-2.5-.5c-.3-.1-.7.1-.9.3l-.6.6c-.2.2-.2.6 0 .8l2.5 2.5 2.5 2.5c.2.2.6.2.8 0l.6-.6c.2-.2.4-.6.3-.9l-.5-2.5 3.5-3.5 4.2 6.2c.2.3.7.5 1.1.3l1.4-.7c.4-.2.6-.6.5-1.1Z"/>',
airportShuttle: '<path d="M2 13h15a2 2 0 0 1 2 2v3H2v-5Z"/><circle cx="6" cy="18" r="1.5"/><circle cx="15" cy="18" r="1.5"/><path d="m14 8 3-4h4l-2 4h4l-1 2H11l2-2Z"/>',
ship: '<path d="M2 20a4 4 0 0 0 4-2 4 4 0 0 1 4-2 4 4 0 0 1 4 2 4 4 0 0 0 4 2 4 4 0 0 0 4-2"/><path d="M4 16 2 10h20l-2 6"/><path d="M12 3v7"/><path d="m8 7 4-4 4 4"/>',
car: '<path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9C2.1 11.1 2 11.5 2 12v4c0 .6.4 1 1 1h2"/><circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/><path d="M5 17h8"/><path d="M10 4h4v3h-4Z"/>',
taxi: '<path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9C2.1 11.1 2 11.5 2 12v4c0 .6.4 1 1 1h2"/><circle cx="7" cy="17" r="2"/><circle cx="17" cy="17" r="2"/><path d="M5 17h8"/><path d="M10 4h4v3h-4Z"/><line x1="8" y1="4" x2="16" y2="4"/>',
// ۲. اقامت و هتل
hotel: '<path d="M3 21h18"/><path d="M5 21V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16"/><path d="M9 7h1"/><path d="M9 11h1"/><path d="M9 15h1"/><path d="M14 7h1"/><path d="M14 11h1"/><path d="M14 15h1"/>',
hotelStar: '<path d="M3 21h18"/><path d="M4 21V7a2 2 0 0 1 2-2h4v16"/><path d="M14 21V11h4a2 2 0 0 1 2 2v8"/><polygon points="12 2 13.1 4.2 15.5 4.5 13.8 6.2 14.2 8.5 12 7.3 9.8 8.5 10.2 6.2 8.5 4.5 10.9 4.2 12 2"/>',
twinBed: '<path d="M2 17h20"/><path d="M2 8v12"/><path d="M22 8v12"/><circle cx="6" cy="11" r="1.5"/><circle cx="18" cy="11" r="1.5"/><path d="M2 14h20"/><path d="M12 14v3"/>',
roomKey: '<circle cx="7.5" cy="15.5" r="4.5"/><path d="m11 12 9-9"/><path d="m17 3 3 3"/><path d="m14 6 2 2"/>',
roomService: '<path d="M2 18h20"/><path d="M12 4v2"/><path d="M4 18a8 8 0 0 1 16 0"/><circle cx="12" cy="4" r="1"/>',
airCondition: '<path d="M2 6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6Z"/><path d="M6 18c1-1 2-1 3 0s2 1 3 0 2-1 3 0 2 1 3 0"/><path d="M8 15c1-1 2-1 3 0s2 1 3 0"/><path d="M18 9h-2"/>',
view: '<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/><path d="M12 9a3 3 0 0 1 3 3"/>',
safeBox: '<rect width="18" height="16" x="3" y="4" rx="2"/><circle cx="12" cy="12" r="3"/><path d="M12 9v3l2 1"/><path d="M7 8h.01"/><path d="M7 12h.01"/><path d="M7 16h.01"/>',
// ۳. پذیرایی و وعده‌های غذایی
meal: '<path d="M18 2v20"/><path d="M18 7h3a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-3"/><path d="M6 2v20"/><path d="M3 2v6a3 3 0 0 0 6 0V2"/>',
buffet: '<path d="M3 17h18"/><path d="M4 17a8 8 0 0 1 16 0"/><path d="M12 5a2 2 0 1 0 0-4 2 2 0 0 0 0 4Z"/><path d="M12 5v4"/>',
breakfast: '<path d="M17 8h1a4 4 0 1 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4V8Z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/>',
hotMeal: '<path d="M3 11h18a1 1 0 0 1 1 1 8 8 0 0 1-8 8H10a8 8 0 0 1-8-8 1 1 0 0 1 1-1Z"/><path d="M7 4c0 1.5 1 2 1 3"/><path d="M12 3c0 1.5 1 2 1 3"/><path d="M17 4c0 1.5 1 2 1 3"/>',
beverage: '<path d="M5 8h14l-2 12H7L5 8Z"/><path d="M4 8h16"/><path d="m14 2-2 6"/>',
halalFood: '<circle cx="12" cy="12" r="9"/><path d="M9 12h6"/><path d="M12 9v6"/><path d="m8.5 8.5 7 7"/>',
snack: '<path d="M12 20.5c-4 0-7-3-7-7.5a6.5 6.5 0 0 1 10.5-5 6.5 6.5 0 0 1 3.5 5c0 4.5-3 7.5-7 7.5Z"/><path d="M12 8c0-3 2-5 4-5"/>',
// ۴. زیارت و اماکن متبرکه
dome: '<path d="M12 2c0 3-4 6-4 10h8c0-4-4-7-4-10Z"/><path d="M4 22V12h16v10"/><path d="M2 22h20"/><circle cx="12" cy="16" r="1.5"/><path d="M12 2V1"/>',
kaaba: '<path d="M12 2 3 7v10l9 5 9-5V7l-9-5Z"/><path d="m3 7 9 5 9-5"/><path d="M12 12v10"/><path d="m7.5 9.5 9-5"/><path d="m7.5 14.5 4.5 2.5"/>',
mosque: '<path d="M3 21h18"/><path d="M4 21V10l2-2 2 2v11"/><path d="M16 21V10l2-2 2 2v11"/><path d="M8 21v-5a4 4 0 0 1 8 0v5"/><path d="M12 5a3 3 0 0 1 3 3H9a3 3 0 0 1 3-3Z"/><path d="M12 2v3"/>',
quran: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1-2.5-2.5Z"/><path d="M6 6h10"/><path d="M6 10h10"/><path d="m11 14 1.5 1.5L15 13"/>',
holyBook: '<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1-2.5-2.5Z"/><path d="M6 6h10"/><path d="M6 10h10"/>',
eulogy: '<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/>',
holyTour: '<circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>',
// ۵. کادر، راهنما و لیدر
tourGuide: '<circle cx="12" cy="7" r="4"/><path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/><path d="m16 11 2 2 4-4"/>',
scholar: '<circle cx="12" cy="9" r="4"/><path d="M6 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/><path d="M8 5a4 4 0 0 1 8 0H8Z"/>',
translator: '<path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/>',
doctor: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="14" x="3" y="6" rx="2"/><path d="M12 10v6"/><path d="M9 13h6"/>',
medical: '<path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="14" x="3" y="6" rx="2"/><path d="M12 10v6"/><path d="M9 13h6"/>',
camera: '<path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/>',
support24: '<path d="M3 18v-6a9 9 0 0 1 18 0v6"/><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/><path d="M10 14h4v2h-4v2h4"/>',
// ۶. اسناد، ویزا و امور مالی
passport: '<rect width="14" height="18" x="5" y="3" rx="2"/><circle cx="12" cy="10" r="3"/><path d="M9 16h6"/><path d="M9 6h6"/>',
visa: '<path d="M4 4h16v16H4Z"/><path d="m9 11 2 2 4-4"/><path d="M7 17h10"/><circle cx="12" cy="12" r="5"/>',
insurance: '<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/><path d="m9 12 2 2 4-4"/>',
currency: '<circle cx="12" cy="12" r="9"/><path d="M14.5 9h-4a1.5 1.5 0 0 0 0 3h3a1.5 1.5 0 0 1 0 3h-4"/><path d="M12 7v10"/>',
tax: '<path d="M4 2v20l2-1 2 1 2-1 2 1 2-1 2 1 2-1 2 1V2l-2 1-2-1-2 1-2-1-2 1-2-1-2 1Z"/><path d="M8 8h8"/><path d="M8 12h8"/><path d="M8 16h4"/>',
// ۷. ارتباطات و تجهیزات مسافر
wifi: '<path d="M12 20h.01"/><path d="M8.5 16.5a5 5 0 0 1 7 0"/><path d="M5 13a10 10 0 0 1 14 0"/><path d="M2 9.5a15 15 0 0 1 20 0"/>',
luggage: '<rect width="16" height="14" x="4" y="7" rx="2"/><path d="M9 7V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v3"/><path d="M4 12h16"/><circle cx="7" cy="21" r="1"/><circle cx="17" cy="21" r="1"/>',
charger: '<rect width="14" height="12" x="5" y="7" rx="2"/><path d="M9 7V3"/><path d="M15 7V3"/><path d="m11 11 2 2-2 2"/>',
waterBottle: '<rect width="8" height="14" x="8" y="7" rx="2"/><path d="M10 7V4h4v3"/><path d="M8 12h8"/><path d="M8 16h8"/>',
umbrella: '<path d="M12 2v20a2 2 0 0 0 2-2"/><path d="M22 13a10 10 0 0 0-20 0Z"/>',
// ۸. تفریح، بازار و گشت
shopping: '<path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4Z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/>',
museum: '<path d="M2 20h20"/><path d="M4 20V10"/><path d="M10 20V10"/><path d="M14 20V10"/><path d="M20 20V10"/><path d="M12 3 2 9h20Z"/>',
gift: '<rect width="18" height="14" x="3" y="8" rx="2"/><path d="M12 8v14"/><path d="M3 13h18"/><path d="M12 8a3 3 0 1 0-3-3c2 0 3 3 3 3Z"/><path d="M12 8a3 3 0 1 1 3-3c-2 0-3 3-3 3Z"/>',
palmTour: '<path d="M13 8c0-2.8-2.2-5-5-5a5 5 0 0 0-5 5c2.8 0 5 2.2 5 5"/><path d="M11 8c0-2.8 2.2-5 5-5a5 5 0 0 1 5 5c-2.8 0-5 2.2-5 5"/><path d="M12 8v14"/>',
} }
export type IconName = keyof typeof PATHS export type IconName = keyof typeof PATHS

Loading…
Cancel
Save