11 changed files with 951 additions and 120 deletions
-
16.agents/rules/iconography_standard.md
-
2.agents/rules/post_code_change_verification.md
-
2AGENTS.md
-
70src/components/shared/ServiceTranslation/ServiceTranslation.tsx
-
2src/components/shared/ServiceTranslation/types.ts
-
247src/components/shared/TourFeatureIconPicker.tsx
-
274src/features/tours/components/CreateTourDialog.tsx
-
381src/features/tours/components/TourDetailDialog.tsx
-
4src/features/tours/services/tour-api.ts
-
5src/features/tours/types.ts
-
68src/icons.tsx
@ -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` (۱۶ پیکسل). |
||||
|
- رنگ و استیتهای فعال، هاور و غیرفعال باید کاملاً منطبق بر سیستم رنگ سازمانی پنل باشند. |
||||
@ -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> |
||||
|
) |
||||
|
} |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue