|
|
|
@ -93,6 +93,7 @@ export default function CustomTripDestinationDetailPage() { |
|
|
|
const [destIsActive, setDestIsActive] = useState(true) |
|
|
|
const [isDestSavedSuccess, setIsDestSavedSuccess] = useState(false) |
|
|
|
const [isDestDeleteOpen, setIsDestDeleteOpen] = useState(false) |
|
|
|
const [isEditDestModalOpen, setIsEditDestModalOpen] = useState(false) |
|
|
|
const [isTogglingStatus, setIsTogglingStatus] = useState(false) |
|
|
|
const [isSavingDestination, setIsSavingDestination] = useState(false) |
|
|
|
|
|
|
|
@ -438,12 +439,9 @@ export default function CustomTripDestinationDetailPage() { |
|
|
|
if (primaryNameEn) setDestNameEn(primaryNameEn) |
|
|
|
if (primaryDesc) setDestDescription(primaryDesc) |
|
|
|
setIsDestSavedSuccess(true) |
|
|
|
|
|
|
|
// Slight smooth delay to let user see the button loading state
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 450)) |
|
|
|
|
|
|
|
// Navigate back to the destinations list tab with a fresh refresh token
|
|
|
|
navigate('/custom-trip/destinations', { state: { refresh: Date.now() } }) |
|
|
|
setIsEditDestModalOpen(false) |
|
|
|
setIsSavingDestination(false) |
|
|
|
setTimeout(() => setIsDestSavedSuccess(false), 3000) |
|
|
|
} catch (err) { |
|
|
|
console.error('Failed to save destination:', err) |
|
|
|
setIsSavingDestination(false) |
|
|
|
@ -872,190 +870,385 @@ export default function CustomTripDestinationDetailPage() { |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* ========================================================================= */} |
|
|
|
{/* TAB 1: DESTINATION INFO */} |
|
|
|
{/* TAB 1: DESTINATION INFO (شناسنامه و مشخصات مقصد) */} |
|
|
|
{/* ========================================================================= */} |
|
|
|
{currentTab === 'info' && ( |
|
|
|
<div className="space-y-6 pt-1 sm:pt-2"> |
|
|
|
<form onSubmit={handleSaveDestinationInfo} className="space-y-4"> |
|
|
|
<div className="rounded-2xl bg-card border border-border/70 p-5 sm:p-6 shadow-xs space-y-5"> |
|
|
|
<div className="flex items-center justify-between border-b border-border/60 pb-4"> |
|
|
|
{/* کارت اصلی شناسنامه شهر */} |
|
|
|
<div className="rounded-2xl bg-card border border-border/70 p-5 sm:p-6 shadow-xs space-y-6"> |
|
|
|
{/* هدر: نام شهر، وضعیت و دکمههای اکشن */} |
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-b border-border/60 pb-5"> |
|
|
|
<div className="flex items-start gap-3"> |
|
|
|
<span className="text-3xl select-none">{destFlag || destination.flag || '📍'}</span> |
|
|
|
<div> |
|
|
|
<h2 className="text-base sm:text-lg font-black text-foreground flex items-center gap-2"> |
|
|
|
<span className="text-2xl select-none">{destFlag || '📍'}</span> |
|
|
|
<span>اطلاعات و شناسنامه شهر {destination.name}</span> |
|
|
|
</h2> |
|
|
|
<p className="text-xs text-muted-foreground mt-0.5"> |
|
|
|
ویرایش مشخصات اصلی، موقعیت کشور و توضیحات مقصد |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
|
|
|
|
{isDestSavedSuccess && ( |
|
|
|
<div className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-emerald-500/10 border border-emerald-500/30 text-emerald-600 text-xs font-bold animate-fadeIn"> |
|
|
|
<Ic name="checkCircle" className="size-4" /> |
|
|
|
<span>تغییرات با موفقیت ذخیره شد</span> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* تنظیمات کلی شهر مقصد (کشور و وضعیت) */} |
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 p-4 rounded-2xl bg-card border border-border/70 shadow-2xs"> |
|
|
|
{/* 1. Country Selection with Radix Select */} |
|
|
|
<div className="space-y-1.5"> |
|
|
|
<label className="text-xs font-bold text-foreground block"> |
|
|
|
کشور مقصد: |
|
|
|
</label> |
|
|
|
<Select |
|
|
|
value={destCountry} |
|
|
|
onValueChange={(val) => { |
|
|
|
setDestCountry(val) |
|
|
|
const preset = PRESET_DEST_COUNTRIES.find((p) => p.name === val) |
|
|
|
if (preset) setDestFlag(preset.flag) |
|
|
|
}} |
|
|
|
> |
|
|
|
<SelectTrigger className="h-10 text-xs rounded-xl bg-card border-border/70 hover:border-border"> |
|
|
|
<SelectValue placeholder="انتخاب کشور مقصد..." /> |
|
|
|
</SelectTrigger> |
|
|
|
<SelectContent> |
|
|
|
{PRESET_DEST_COUNTRIES.map((c) => ( |
|
|
|
<SelectItem key={c.name} value={c.name}> |
|
|
|
<span className="flex items-center gap-2"> |
|
|
|
<span className="text-base select-none">{c.flag}</span> |
|
|
|
<span className="font-medium">{c.name}</span> |
|
|
|
</span> |
|
|
|
</SelectItem> |
|
|
|
))} |
|
|
|
</SelectContent> |
|
|
|
</Select> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* 2. Active Status Toggle Switch with Mini Spinner */} |
|
|
|
<div className="space-y-1.5"> |
|
|
|
<label className="text-xs font-bold text-foreground block"> |
|
|
|
وضعیت شهر در سامانه: |
|
|
|
</label> |
|
|
|
<div className="flex h-10 items-center justify-between px-3.5 rounded-xl border border-border/70 bg-card"> |
|
|
|
<div className="flex items-center gap-2"> |
|
|
|
<div className="flex items-center gap-2.5 flex-wrap"> |
|
|
|
<h2 className="text-lg sm:text-xl font-black text-foreground"> |
|
|
|
{destination.name} |
|
|
|
</h2> |
|
|
|
{destination.nameEn && ( |
|
|
|
<span className="text-xs font-mono text-muted-foreground font-normal"> |
|
|
|
({destination.nameEn}) |
|
|
|
</span> |
|
|
|
)} |
|
|
|
<span |
|
|
|
className={cn( |
|
|
|
'inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-semibold', |
|
|
|
destIsActive |
|
|
|
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border border-emerald-500/20' |
|
|
|
: 'bg-muted text-muted-foreground border border-border-soft' |
|
|
|
)} |
|
|
|
> |
|
|
|
<span |
|
|
|
className={cn( |
|
|
|
'size-2 rounded-full', |
|
|
|
'size-1.5 rounded-full', |
|
|
|
destIsActive ? 'bg-emerald-500 animate-pulse' : 'bg-muted-foreground' |
|
|
|
)} |
|
|
|
/> |
|
|
|
<span |
|
|
|
className={cn( |
|
|
|
'text-xs font-bold', |
|
|
|
destIsActive |
|
|
|
? 'text-emerald-600 dark:text-emerald-400' |
|
|
|
: 'text-muted-foreground' |
|
|
|
)} |
|
|
|
> |
|
|
|
{destIsActive ? 'شهر فعال' : 'شهر غیرفعال'} |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<Switch |
|
|
|
checked={destIsActive} |
|
|
|
onCheckedChange={handleToggleStatus} |
|
|
|
isLoading={isTogglingStatus} |
|
|
|
disabled={isTogglingStatus} |
|
|
|
aria-label="تغییر وضعیت فعال یا غیرفعال بودن شهر مقصد" |
|
|
|
/> |
|
|
|
<span>{destIsActive ? 'شهر فعال در سامانه' : 'شهر غیرفعال'}</span> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<p className="text-xs text-muted-foreground mt-1"> |
|
|
|
شناسنامه شهر، اماکن متبرکه، هتلهای مرتبط و ترجمههای بینالمللی مقصد |
|
|
|
</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* ماژول چندزبانه ترجمه با ServiceTranslation */} |
|
|
|
<div className="space-y-4 rounded-2xl border border-border/70 bg-card p-4 sm:p-5"> |
|
|
|
<div className="border-b border-border/60 pb-3"> |
|
|
|
<ServiceTranslation |
|
|
|
activeLang={activeLangTab} |
|
|
|
onLangChange={(lang) => setActiveLangTab(lang)} |
|
|
|
completedLangs={{ |
|
|
|
ar: Boolean(names.ar?.trim()), |
|
|
|
fa: Boolean(names.fa?.trim()), |
|
|
|
en: Boolean(names.en?.trim()), |
|
|
|
ru: Boolean(names.ru?.trim()), |
|
|
|
id: Boolean(names.id?.trim()), |
|
|
|
}} |
|
|
|
primaryLang="ar" |
|
|
|
title="اطلاعات متنی و چندزبانه مقصد" |
|
|
|
required |
|
|
|
variant="pills" |
|
|
|
{/* دکمههای عملیات */} |
|
|
|
<div className="flex items-center gap-2 shrink-0"> |
|
|
|
<Button |
|
|
|
onClick={() => setIsEditDestModalOpen(true)} |
|
|
|
className="rounded-xl h-10 px-4 text-xs sm:text-sm font-bold gap-2 bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs cursor-pointer" |
|
|
|
> |
|
|
|
<Ic name="pencil" className="size-4" /> |
|
|
|
<span>ویرایش در پاپآپ</span> |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant="outline" |
|
|
|
onClick={() => setIsDestDeleteOpen(true)} |
|
|
|
className="rounded-xl h-10 px-3 text-xs font-medium text-rose-600 hover:bg-rose-500/10 hover:text-rose-700 border-border/70 cursor-pointer" |
|
|
|
title="حذف مقصد" |
|
|
|
> |
|
|
|
<Ic name="trash" className="size-4" /> |
|
|
|
</Button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* اعلان موفقیت ذخیره در صورت وجود */} |
|
|
|
{isDestSavedSuccess && ( |
|
|
|
<div className="flex items-center gap-2 p-3 rounded-xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-600 text-xs font-bold animate-fadeIn"> |
|
|
|
<Ic name="checkCircle" className="size-4 shrink-0" /> |
|
|
|
<span>مشخصات شهر مقصد با موفقیت در سامانه ذخیره و بهروزرسانی شد.</span> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* کارتهای آماری سریع */} |
|
|
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3"> |
|
|
|
<div className="p-3.5 rounded-xl bg-muted/30 border border-border/60"> |
|
|
|
<span className="text-[11px] text-muted-foreground block">کشور مقصد</span> |
|
|
|
<span className="text-sm font-bold text-foreground mt-1 flex items-center gap-1.5"> |
|
|
|
<span className="text-base select-none">{destFlag || destination.flag || '📍'}</span> |
|
|
|
<span>{destCountry || destination.country}</span> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
onClick={() => handleTabChange('hotels')} |
|
|
|
className="p-3.5 rounded-xl bg-muted/30 border border-border/60 cursor-pointer hover:border-primary/50 transition-colors" |
|
|
|
title="مشاهده هتلها" |
|
|
|
> |
|
|
|
<span className="text-[11px] text-muted-foreground block">هتلهای فعال</span> |
|
|
|
<span className="text-sm font-mono font-bold text-foreground mt-1 flex items-center gap-1.5"> |
|
|
|
<Ic name="bed" className="size-4 text-primary" /> |
|
|
|
<span>{toFa(destHotels.length)} هتل</span> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
onClick={() => handleTabChange('transports')} |
|
|
|
className="p-3.5 rounded-xl bg-muted/30 border border-border/60 cursor-pointer hover:border-primary/50 transition-colors" |
|
|
|
title="مشاهده مسیرهای حملونقل" |
|
|
|
> |
|
|
|
<span className="text-[11px] text-muted-foreground block">مسیرهای متصل</span> |
|
|
|
<span className="text-sm font-mono font-bold text-foreground mt-1 flex items-center gap-1.5"> |
|
|
|
<Ic name="route" className="size-4 text-primary" /> |
|
|
|
<span>{toFa(destRelations.length)} مسیر</span> |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<div className="p-3.5 rounded-xl bg-muted/30 border border-border/60"> |
|
|
|
<span className="text-[11px] text-muted-foreground block">وضعیت در سامانه</span> |
|
|
|
<div className="mt-1 flex items-center justify-between"> |
|
|
|
<span className="text-xs font-bold text-foreground"> |
|
|
|
{destIsActive ? 'فعال' : 'غیرفعال'} |
|
|
|
</span> |
|
|
|
<Switch |
|
|
|
checked={destIsActive} |
|
|
|
onCheckedChange={handleToggleStatus} |
|
|
|
isLoading={isTogglingStatus} |
|
|
|
disabled={isTogglingStatus} |
|
|
|
aria-label="تغییر وضعیت فعال یا غیرفعال بودن شهر مقصد" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* ماژول نمایش مشخصات چندزبانه با ServiceTranslation در حالت Readonly */} |
|
|
|
<div className="space-y-4 rounded-2xl border border-border/70 bg-card p-4 sm:p-5"> |
|
|
|
<div className="border-b border-border/60 pb-3"> |
|
|
|
<ServiceTranslation |
|
|
|
activeLang={activeLangTab} |
|
|
|
onLangChange={(lang) => setActiveLangTab(lang)} |
|
|
|
completedLangs={{ |
|
|
|
ar: Boolean(names.ar?.trim()), |
|
|
|
fa: Boolean(names.fa?.trim()), |
|
|
|
en: Boolean(names.en?.trim()), |
|
|
|
ru: Boolean(names.ru?.trim()), |
|
|
|
id: Boolean(names.id?.trim()), |
|
|
|
}} |
|
|
|
primaryLang="ar" |
|
|
|
title="شناسنامه و متون چندزبانه مقصد" |
|
|
|
variant="pills" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* محتوای زبان انتخابشده */} |
|
|
|
<div className="space-y-4 pt-1" dir={currentLang.dir}> |
|
|
|
<div className="space-y-1"> |
|
|
|
<span className="text-[11px] text-muted-foreground font-semibold block"> |
|
|
|
نام شهر مقصد ({currentLang.name}): |
|
|
|
</span> |
|
|
|
<div className="p-3 rounded-xl bg-muted/40 border border-border/60 text-sm font-bold text-foreground"> |
|
|
|
{names[activeLangTab] || destination.name || '---'} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* فیلدهای ترجمهپذیر متناسب با زبان تب فعال */} |
|
|
|
<div className="space-y-4 pt-1" dir={currentLang.dir}> |
|
|
|
{/* نام شهر مقصد به زبان انتخابشده */} |
|
|
|
{holyPlacesMap[activeLangTab] && ( |
|
|
|
<div className="space-y-1.5"> |
|
|
|
<div className="flex items-center justify-between"> |
|
|
|
<label className="text-xs font-bold text-foreground flex items-center gap-1.5"> |
|
|
|
<Ic name="pencil" className="size-3.5 text-muted-foreground" /> |
|
|
|
<span>نام شهر مقصد ({currentLang.name})</span> |
|
|
|
<span className="text-[10px] px-1.5 py-0.5 rounded font-mono font-bold bg-primary/10 text-primary border border-primary/20"> |
|
|
|
{currentLang.badge} |
|
|
|
</span> |
|
|
|
<span className="text-rose-500">*</span> |
|
|
|
</label> |
|
|
|
<span className="text-[11px] text-muted-foreground font-mono"> |
|
|
|
{currentLang.dir.toUpperCase()} |
|
|
|
</span> |
|
|
|
<span className="text-[11px] text-muted-foreground font-semibold block"> |
|
|
|
اماکن مقدسه و زیارتگاهها ({currentLang.name}): |
|
|
|
</span> |
|
|
|
<div className="flex flex-wrap gap-1.5 p-3 rounded-xl bg-muted/40 border border-border/60"> |
|
|
|
{holyPlacesMap[activeLangTab] |
|
|
|
.split(/[،,]/) |
|
|
|
.map((p) => p.trim()) |
|
|
|
.filter(Boolean) |
|
|
|
.map((place, idx) => ( |
|
|
|
<span |
|
|
|
key={idx} |
|
|
|
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-lg bg-card border border-border/70 text-xs font-semibold text-foreground shadow-2xs" |
|
|
|
> |
|
|
|
<span>🕌</span> |
|
|
|
<span>{place}</span> |
|
|
|
</span> |
|
|
|
))} |
|
|
|
</div> |
|
|
|
<Input |
|
|
|
value={names[activeLangTab] || ''} |
|
|
|
onChange={(e) => |
|
|
|
setNames((prev) => ({ ...prev, [activeLangTab]: e.target.value })) |
|
|
|
} |
|
|
|
dir={currentLang.dir} |
|
|
|
placeholder={`نام شهر به زبان ${currentLang.name}...`} |
|
|
|
className="h-10 rounded-xl text-xs bg-card border-border/70 focus-visible:border-primary focus-visible:ring-1 focus-visible:ring-primary shadow-2xs" |
|
|
|
required={activeLangTab === 'fa' || activeLangTab === 'ar'} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
)} |
|
|
|
|
|
|
|
{/* توضیحات و راهنمای شهر به زبان انتخابشده */} |
|
|
|
<div className="space-y-1.5"> |
|
|
|
<label className="text-xs font-bold text-foreground block"> |
|
|
|
توضیحات و راهنمای شهر ({currentLang.name}): |
|
|
|
</label> |
|
|
|
<textarea |
|
|
|
value={descriptionsMap[activeLangTab] || ''} |
|
|
|
onChange={(e) => |
|
|
|
setDescriptionsMap((prev) => ({ ...prev, [activeLangTab]: e.target.value })) |
|
|
|
} |
|
|
|
dir={currentLang.dir} |
|
|
|
rows={4} |
|
|
|
placeholder={`شرح کوتاه اماکن مقدسه، شرایط سفر و ویژگیهای شهر به زبان ${currentLang.name}...`} |
|
|
|
className="w-full p-3 text-xs rounded-xl bg-card border border-border/70 text-foreground placeholder:text-muted-foreground leading-relaxed focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none resize-y shadow-2xs" |
|
|
|
/> |
|
|
|
<div className="space-y-1"> |
|
|
|
<span className="text-[11px] text-muted-foreground font-semibold block"> |
|
|
|
توضیحات و راهنمای شهر ({currentLang.name}): |
|
|
|
</span> |
|
|
|
<div className="p-3.5 rounded-xl bg-muted/40 border border-border/60 text-xs text-foreground leading-relaxed whitespace-pre-wrap"> |
|
|
|
{descriptionsMap[activeLangTab] || destination.description || 'توضیحاتی برای این زبان ثبت نشده است.'} |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* Save Footer Bar */} |
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 pt-3 border-t border-border/60"> |
|
|
|
<Button |
|
|
|
type="submit" |
|
|
|
isLoading={isSavingDestination} |
|
|
|
loadingText={t('common.loading')} |
|
|
|
disabled={isSavingDestination} |
|
|
|
className="h-10 px-6 rounded-xl font-bold text-xs sm:text-sm bg-primary text-white hover:bg-primary/90 shadow-xs" |
|
|
|
> |
|
|
|
<Ic name="check" className="size-4" /> |
|
|
|
<span>ذخیره و ثبت تغییرات شهر مقصد</span> |
|
|
|
</Button> |
|
|
|
{/* ========================================================================= */} |
|
|
|
{/* MODAL: EDIT DESTINATION INFO (طبق مدل popup.tsx / dialog.tsx) */} |
|
|
|
{/* ========================================================================= */} |
|
|
|
<Dialog open={isEditDestModalOpen} onOpenChange={setIsEditDestModalOpen}> |
|
|
|
<DialogContent className="sm:max-w-xl md:max-w-2xl w-full rounded-2xl bg-card border border-border-soft p-0 max-h-[88vh] flex flex-col overflow-hidden shadow-2xl"> |
|
|
|
{/* هدر ثابت مدال */} |
|
|
|
<DialogHeader className="p-5 sm:p-6 pb-4 pe-12 border-b border-border/60 shrink-0 bg-card text-start"> |
|
|
|
<div className="flex items-center gap-3"> |
|
|
|
<div className="size-10 rounded-xl bg-primary/10 text-primary flex items-center justify-center shrink-0"> |
|
|
|
<Ic name="map" className="size-5" /> |
|
|
|
</div> |
|
|
|
<div> |
|
|
|
<DialogTitle className="text-base sm:text-lg font-bold text-foreground"> |
|
|
|
ویرایش اطلاعات و شناسنامه شهر {destination.name} |
|
|
|
</DialogTitle> |
|
|
|
<DialogDescription className="text-xs text-muted-foreground mt-0.5"> |
|
|
|
ویرایش مشخصات اصلی، موقعیت کشور و توضیحات چندزبانه مقصد |
|
|
|
</DialogDescription> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</DialogHeader> |
|
|
|
|
|
|
|
{/* فرم: بدنه اسکرولشونده و فوتر فیکسشده */} |
|
|
|
<form onSubmit={handleSaveDestinationInfo} className="flex flex-col flex-1 overflow-hidden min-h-0"> |
|
|
|
<div className="flex-1 overflow-y-auto p-5 sm:p-6 space-y-4 text-xs sm:text-sm"> |
|
|
|
{/* تنظیمات کلی شهر مقصد (کشور و وضعیت) */} |
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 p-3.5 rounded-xl bg-card border border-border/70 shadow-2xs"> |
|
|
|
{/* انتخاب کشور مقصد */} |
|
|
|
<div className="space-y-1.5"> |
|
|
|
<label className="text-xs font-bold text-foreground block"> |
|
|
|
کشور مقصد: |
|
|
|
</label> |
|
|
|
<Select |
|
|
|
value={destCountry} |
|
|
|
onValueChange={(val) => { |
|
|
|
setDestCountry(val) |
|
|
|
const preset = PRESET_DEST_COUNTRIES.find((p) => p.name === val) |
|
|
|
if (preset) setDestFlag(preset.flag) |
|
|
|
}} |
|
|
|
> |
|
|
|
<SelectTrigger className="h-10 text-xs rounded-xl bg-card border-border/70 hover:border-border"> |
|
|
|
<SelectValue placeholder="انتخاب کشور مقصد..." /> |
|
|
|
</SelectTrigger> |
|
|
|
<SelectContent> |
|
|
|
{PRESET_DEST_COUNTRIES.map((c) => ( |
|
|
|
<SelectItem key={c.name} value={c.name}> |
|
|
|
<span className="flex items-center gap-2"> |
|
|
|
<span className="text-base select-none">{c.flag}</span> |
|
|
|
<span className="font-medium">{c.name}</span> |
|
|
|
</span> |
|
|
|
</SelectItem> |
|
|
|
))} |
|
|
|
</SelectContent> |
|
|
|
</Select> |
|
|
|
</div> |
|
|
|
|
|
|
|
<Button |
|
|
|
type="button" |
|
|
|
variant="ghost" |
|
|
|
onClick={() => setIsDestDeleteOpen(true)} |
|
|
|
className="h-10 px-4 rounded-xl text-xs font-bold text-rose-600 hover:bg-rose-500/10 hover:text-rose-700" |
|
|
|
> |
|
|
|
<Ic name="trash" className="size-4" /> |
|
|
|
<span>حذف این شهر مقصد</span> |
|
|
|
</Button> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</form> |
|
|
|
{/* وضعیت شهر در سامانه */} |
|
|
|
<div className="space-y-1.5"> |
|
|
|
<label className="text-xs font-bold text-foreground block"> |
|
|
|
وضعیت شهر در سامانه: |
|
|
|
</label> |
|
|
|
<div className="flex h-10 items-center justify-between px-3.5 rounded-xl border border-border/70 bg-card"> |
|
|
|
<div className="flex items-center gap-2"> |
|
|
|
<span |
|
|
|
className={cn( |
|
|
|
'size-2 rounded-full', |
|
|
|
destIsActive ? 'bg-emerald-500 animate-pulse' : 'bg-muted-foreground' |
|
|
|
)} |
|
|
|
/> |
|
|
|
<span |
|
|
|
className={cn( |
|
|
|
'text-xs font-bold', |
|
|
|
destIsActive |
|
|
|
? 'text-emerald-600 dark:text-emerald-400' |
|
|
|
: 'text-muted-foreground' |
|
|
|
)} |
|
|
|
> |
|
|
|
{destIsActive ? 'شهر فعال' : 'شهر غیرفعال'} |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<Switch |
|
|
|
checked={destIsActive} |
|
|
|
onCheckedChange={setDestIsActive} |
|
|
|
aria-label="تغییر وضعیت فعال بودن شهر مقصد" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* ماژول چندزبانه ترجمه */} |
|
|
|
<div className="space-y-4 rounded-2xl border border-border/70 bg-card p-4"> |
|
|
|
<div className="border-b border-border/60 pb-3"> |
|
|
|
<ServiceTranslation |
|
|
|
activeLang={activeLangTab} |
|
|
|
onLangChange={(lang) => setActiveLangTab(lang)} |
|
|
|
completedLangs={{ |
|
|
|
ar: Boolean(names.ar?.trim()), |
|
|
|
fa: Boolean(names.fa?.trim()), |
|
|
|
en: Boolean(names.en?.trim()), |
|
|
|
ru: Boolean(names.ru?.trim()), |
|
|
|
id: Boolean(names.id?.trim()), |
|
|
|
}} |
|
|
|
primaryLang="ar" |
|
|
|
title="اطلاعات متنی و چندزبانه مقصد" |
|
|
|
required |
|
|
|
variant="pills" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* فیلدهای ترجمهپذیر متناسب با زبان فعال */} |
|
|
|
<div className="space-y-3.5 pt-1" dir={currentLang.dir}> |
|
|
|
<div className="space-y-1.5"> |
|
|
|
<div className="flex items-center justify-between"> |
|
|
|
<label className="text-xs font-bold text-foreground flex items-center gap-1.5"> |
|
|
|
<Ic name="pencil" className="size-3.5 text-muted-foreground" /> |
|
|
|
<span>نام شهر مقصد ({currentLang.name})</span> |
|
|
|
<span className="text-[10px] px-1.5 py-0.5 rounded font-mono font-bold bg-primary/10 text-primary border border-primary/20"> |
|
|
|
{currentLang.badge} |
|
|
|
</span> |
|
|
|
<span className="text-rose-500">*</span> |
|
|
|
</label> |
|
|
|
<span className="text-[11px] text-muted-foreground font-mono"> |
|
|
|
{currentLang.dir.toUpperCase()} |
|
|
|
</span> |
|
|
|
</div> |
|
|
|
<Input |
|
|
|
value={names[activeLangTab] || ''} |
|
|
|
onChange={(e) => |
|
|
|
setNames((prev) => ({ ...prev, [activeLangTab]: e.target.value })) |
|
|
|
} |
|
|
|
dir={currentLang.dir} |
|
|
|
placeholder={`نام شهر به زبان ${currentLang.name}...`} |
|
|
|
className="h-10 rounded-xl text-xs bg-card border-border/70 focus-visible:border-primary focus-visible:ring-1 focus-visible:ring-primary shadow-2xs" |
|
|
|
required={activeLangTab === 'fa' || activeLangTab === 'ar'} |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="space-y-1.5"> |
|
|
|
<label className="text-xs font-bold text-foreground flex items-center gap-1.5"> |
|
|
|
<span>اماکن مقدسه و زیارتگاهها ({currentLang.name})</span> |
|
|
|
<span className="text-[11px] text-muted-foreground font-normal">(با ویرگول جدا کنید)</span> |
|
|
|
</label> |
|
|
|
<Input |
|
|
|
value={holyPlacesMap[activeLangTab] || ''} |
|
|
|
onChange={(e) => |
|
|
|
setHolyPlacesMap((prev) => ({ ...prev, [activeLangTab]: e.target.value })) |
|
|
|
} |
|
|
|
dir={currentLang.dir} |
|
|
|
placeholder={`اماکن متبرکه به زبان ${currentLang.name}...`} |
|
|
|
className="h-10 rounded-xl text-xs bg-card border-border/70 focus-visible:border-primary focus-visible:ring-1 focus-visible:ring-primary shadow-2xs" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
|
|
|
|
<div className="space-y-1.5"> |
|
|
|
<label className="text-xs font-bold text-foreground block"> |
|
|
|
توضیحات و راهنمای شهر ({currentLang.name}): |
|
|
|
</label> |
|
|
|
<textarea |
|
|
|
value={descriptionsMap[activeLangTab] || ''} |
|
|
|
onChange={(e) => |
|
|
|
setDescriptionsMap((prev) => ({ ...prev, [activeLangTab]: e.target.value })) |
|
|
|
} |
|
|
|
dir={currentLang.dir} |
|
|
|
rows={3} |
|
|
|
placeholder={`شرح کوتاه اماکن مقدسه، شرایط سفر و ویژگیهای شهر به زبان ${currentLang.name}...`} |
|
|
|
className="w-full p-3 text-xs rounded-xl bg-card border border-border/70 text-foreground placeholder:text-muted-foreground leading-relaxed focus:border-primary focus:ring-1 focus:ring-primary focus:outline-none resize-y shadow-2xs" |
|
|
|
/> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
|
|
|
|
{/* فوتر فیکسشده در پایین مدال */} |
|
|
|
<DialogFooter className="p-4 sm:p-5 border-t border-border-soft bg-card/95 backdrop-blur-xs flex items-center justify-end gap-2.5 shrink-0 mt-0"> |
|
|
|
<Button |
|
|
|
type="button" |
|
|
|
variant="outline" |
|
|
|
onClick={() => setIsEditDestModalOpen(false)} |
|
|
|
className="rounded-xl text-xs h-9 px-4 border-border-soft hover:bg-muted" |
|
|
|
> |
|
|
|
انصراف |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
type="submit" |
|
|
|
isLoading={isSavingDestination} |
|
|
|
loadingText={t('common.loading')} |
|
|
|
className="rounded-xl text-xs font-bold h-9 px-5 bg-primary text-primary-foreground hover:bg-primary/90 shadow-xs" |
|
|
|
> |
|
|
|
<Ic name="check" className="size-4" /> |
|
|
|
<span>ذخیره و ثبت تغییرات شهر مقصد</span> |
|
|
|
</Button> |
|
|
|
</DialogFooter> |
|
|
|
</form> |
|
|
|
</DialogContent> |
|
|
|
</Dialog> |
|
|
|
|
|
|
|
{/* Delete Destination Dialog */} |
|
|
|
<DeleteConfirmDialog |
|
|
|
|