You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
641 lines
26 KiB
641 lines
26 KiB
import * as React from 'react'
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogClose,
|
|
} from '@/components/ui/dialog'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Ic } from '@/icons'
|
|
import { Shimmer } from '@/components/shared/shimmer_frontend'
|
|
import { TourDetailOverviewShimmer } from './TourDetailShimmer'
|
|
import {
|
|
fetchTourTranslations,
|
|
saveTourTranslations,
|
|
fetchTourItineraryTranslations,
|
|
saveTourItineraryTranslation,
|
|
} from '../services/tour-api'
|
|
import type {
|
|
TourTranslationsResponse,
|
|
TourTranslationData,
|
|
TourItineraryTranslationsResponseItem,
|
|
} from '../types'
|
|
|
|
interface TourTranslationsDialogProps {
|
|
tourId: number | null
|
|
tourTitle?: string
|
|
open: boolean
|
|
onOpenChange: (open: boolean) => void
|
|
onSuccess?: () => void
|
|
}
|
|
|
|
const SUPPORTED_LANGUAGES = [
|
|
{ code: 'ar', label: 'العربية', flag: '🇸🇦', dir: 'rtl' },
|
|
{ code: 'fa', label: 'فارسی', flag: '🇮🇷', dir: 'rtl' },
|
|
{ code: 'en', label: 'English', flag: '🇬🇧', dir: 'ltr' },
|
|
{ code: 'ru', label: 'Русский', flag: '🇷🇺', dir: 'ltr' },
|
|
{ code: 'id', label: 'Indonesia', flag: '🇮🇩', dir: 'ltr' },
|
|
] as const
|
|
|
|
type LangCode = (typeof SUPPORTED_LANGUAGES)[number]['code']
|
|
|
|
export function TourTranslationsDialog({
|
|
tourId,
|
|
tourTitle,
|
|
open,
|
|
onOpenChange,
|
|
onSuccess,
|
|
}: TourTranslationsDialogProps) {
|
|
const [selectedLang, setSelectedLang] = React.useState<LangCode>('ar')
|
|
const [activeSection, setActiveSection] = React.useState<'base' | 'features' | 'tips' | 'itinerary'>('base')
|
|
const [isLoading, setIsLoading] = React.useState<boolean>(false)
|
|
const [isSaving, setIsSaving] = React.useState<boolean>(false)
|
|
const [errorMessage, setErrorMessage] = React.useState<string | null>(null)
|
|
const [successMessage, setSuccessMessage] = React.useState<string | null>(null)
|
|
|
|
// Translations data
|
|
const [transData, setTransData] = React.useState<TourTranslationsResponse | null>(null)
|
|
const [itineraries, setItineraries] = React.useState<TourItineraryTranslationsResponseItem[]>([])
|
|
|
|
// Local draft states for current selected language
|
|
const [formTitle, setFormTitle] = React.useState<string>('')
|
|
const [formDesc, setFormDesc] = React.useState<string>('')
|
|
const [formFeatures, setFormFeatures] = React.useState<string[]>([])
|
|
const [newFeatureText, setNewFeatureText] = React.useState<string>('')
|
|
const [formTips, setFormTips] = React.useState<{ title: string; desc: string }[]>([])
|
|
const [newTipTitle, setNewTipTitle] = React.useState<string>('')
|
|
const [newTipDesc, setNewTipDesc] = React.useState<string>('')
|
|
|
|
// Drafts for itineraries: { [itineraryId]: { title: string, summary: string } }
|
|
const [itinDrafts, setItinDrafts] = React.useState<Record<number, { title: string; summary: string }>>({})
|
|
|
|
// Load translations on open
|
|
React.useEffect(() => {
|
|
if (!open || !tourId) return
|
|
|
|
let isMounted = true
|
|
setIsLoading(true)
|
|
setErrorMessage(null)
|
|
setSuccessMessage(null)
|
|
|
|
Promise.all([
|
|
fetchTourTranslations(tourId),
|
|
fetchTourItineraryTranslations(tourId).catch(() => []),
|
|
])
|
|
.then(([tourTrans, itinTrans]) => {
|
|
if (!isMounted) return
|
|
setTransData(tourTrans)
|
|
setItineraries(itinTrans || [])
|
|
|
|
// Default to a language different from primary if primary is English/Persian
|
|
const defaultLang = tourTrans.primary_language === 'fa' ? 'ar' : 'fa'
|
|
setSelectedLang(defaultLang as LangCode)
|
|
initDraftForLanguage(defaultLang as LangCode, tourTrans, itinTrans || [])
|
|
})
|
|
.catch((err) => {
|
|
if (!isMounted) return
|
|
setErrorMessage(err?.message || 'خطا در بارگذاری ترجمههای تور از سرور.')
|
|
})
|
|
.finally(() => {
|
|
if (isMounted) setIsLoading(false)
|
|
})
|
|
|
|
return () => {
|
|
isMounted = false
|
|
}
|
|
}, [open, tourId])
|
|
|
|
// Sync draft whenever selectedLang changes
|
|
const initDraftForLanguage = (
|
|
lang: LangCode,
|
|
tourTrans: TourTranslationsResponse | null,
|
|
itinList: TourItineraryTranslationsResponseItem[]
|
|
) => {
|
|
if (!tourTrans) return
|
|
|
|
const isPrimary = tourTrans.primary_language === lang
|
|
const existingTour = isPrimary
|
|
? {
|
|
title: tourTrans.primary_data.title || '',
|
|
description: tourTrans.primary_data.description || '',
|
|
tour_feature: tourTrans.primary_data.tour_feature || [],
|
|
travel_tips: tourTrans.primary_data.travel_tips || {},
|
|
}
|
|
: tourTrans.translations[lang] || {
|
|
title: '',
|
|
description: '',
|
|
tour_feature: [],
|
|
travel_tips: {},
|
|
}
|
|
|
|
setFormTitle(existingTour.title || '')
|
|
setFormDesc(existingTour.description || '')
|
|
|
|
const featuresList = (existingTour.tour_feature || []).map((f) => f.title)
|
|
setFormFeatures(featuresList)
|
|
|
|
const tipsList = Object.entries(existingTour.travel_tips || {}).map(([title, desc]) => ({
|
|
title,
|
|
desc: String(desc),
|
|
}))
|
|
setFormTips(tipsList)
|
|
|
|
// Init itinerary drafts for this language
|
|
const drafts: Record<number, { title: string; summary: string }> = {}
|
|
itinList.forEach((it) => {
|
|
const itTrans = it.translations[lang]
|
|
drafts[it.id] = {
|
|
title: itTrans?.title || '',
|
|
summary: itTrans?.summary || '',
|
|
}
|
|
})
|
|
setItinDrafts(drafts)
|
|
}
|
|
|
|
const handleLanguageChange = (lang: LangCode) => {
|
|
setSelectedLang(lang)
|
|
setErrorMessage(null)
|
|
setSuccessMessage(null)
|
|
initDraftForLanguage(lang, transData, itineraries)
|
|
}
|
|
|
|
const handleAddFeature = () => {
|
|
if (!newFeatureText.trim()) return
|
|
setFormFeatures((prev) => [...prev, newFeatureText.trim()])
|
|
setNewFeatureText('')
|
|
}
|
|
|
|
const handleRemoveFeature = (idx: number) => {
|
|
setFormFeatures((prev) => prev.filter((_, i) => i !== idx))
|
|
}
|
|
|
|
const handleAddTip = () => {
|
|
if (!newTipTitle.trim() || !newTipDesc.trim()) return
|
|
setFormTips((prev) => [...prev, { title: newTipTitle.trim(), desc: newTipDesc.trim() }])
|
|
setNewTipTitle('')
|
|
setNewTipDesc('')
|
|
}
|
|
|
|
const handleRemoveTip = (idx: number) => {
|
|
setFormTips((prev) => prev.filter((_, i) => i !== idx))
|
|
}
|
|
|
|
const handleItineraryDraftChange = (itId: number, field: 'title' | 'summary', val: string) => {
|
|
setItinDrafts((prev) => ({
|
|
...prev,
|
|
[itId]: {
|
|
title: prev[itId]?.title || '',
|
|
summary: prev[itId]?.summary || '',
|
|
[field]: val,
|
|
},
|
|
}))
|
|
}
|
|
|
|
const handleSave = async () => {
|
|
if (!tourId || !transData) return
|
|
if (!formTitle.trim() && !isPrimary) {
|
|
setErrorMessage(`وارد کردن عنوان تور برای زبان ${activeLangConfig?.label || selectedLang} اجباری است.`)
|
|
return
|
|
}
|
|
setIsSaving(true)
|
|
setErrorMessage(null)
|
|
setSuccessMessage(null)
|
|
|
|
try {
|
|
// Build tour_feature array
|
|
const tour_feature = formFeatures.map((title) => ({ title }))
|
|
|
|
// Build travel_tips object
|
|
const travel_tips: Record<string, string> = {}
|
|
formTips.forEach((item) => {
|
|
travel_tips[item.title] = item.desc
|
|
})
|
|
|
|
// 1. Save Tour translation
|
|
await saveTourTranslations(tourId, {
|
|
lang_code: selectedLang,
|
|
title: formTitle.trim(),
|
|
description: formDesc.trim(),
|
|
tour_feature,
|
|
travel_tips,
|
|
})
|
|
|
|
// 2. Save Itinerary translations
|
|
const itinSavePromises = Object.entries(itinDrafts).map(([itIdStr, draft]) => {
|
|
const itId = Number(itIdStr)
|
|
if (!draft.title.trim() && !draft.summary.trim()) return Promise.resolve()
|
|
return saveTourItineraryTranslation(tourId, {
|
|
itinerary_id: itId,
|
|
lang_code: selectedLang,
|
|
title: draft.title.trim(),
|
|
summary: draft.summary.trim(),
|
|
})
|
|
})
|
|
|
|
await Promise.all(itinSavePromises)
|
|
|
|
// Refresh data
|
|
const [updatedTour, updatedItin] = await Promise.all([
|
|
fetchTourTranslations(tourId),
|
|
fetchTourItineraryTranslations(tourId).catch(() => []),
|
|
])
|
|
setTransData(updatedTour)
|
|
setItineraries(updatedItin || [])
|
|
|
|
setSuccessMessage(`ترجمه زبان ${SUPPORTED_LANGUAGES.find((l) => l.code === selectedLang)?.label} با موفقیت در دیتابیس ذخیره شد.`)
|
|
if (onSuccess) onSuccess()
|
|
} catch (err: any) {
|
|
setErrorMessage(err?.message || 'خطا در ذخیره ترجمه در سرور.')
|
|
} finally {
|
|
setIsSaving(false)
|
|
}
|
|
}
|
|
|
|
const activeLangConfig = SUPPORTED_LANGUAGES.find((l) => l.code === selectedLang)
|
|
const isPrimary = transData?.primary_language === selectedLang
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-[760px] p-0 overflow-hidden max-h-[90vh] flex flex-col">
|
|
{/* Dialog Header */}
|
|
<DialogHeader className="px-6 pt-5 pb-4 border-b border-border/50 bg-secondary/15">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2.5">
|
|
<div className="flex size-9 items-center justify-center rounded-xl bg-primary/10 text-primary">
|
|
<Ic name="globe" className="size-5" />
|
|
</div>
|
|
<div>
|
|
<DialogTitle className="text-base font-extrabold text-foreground">
|
|
مدیریت ترجمههای 5 زبانه تور
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs text-grey-3 mt-0.5">
|
|
{tourTitle || `تور شناسه #${tourId}`} • ذخیرهسازی پویا در سرور و پایگاهداده
|
|
</DialogDescription>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{/* Primary Language Indicator */}
|
|
{transData && (
|
|
<div className="flex items-center gap-1.5 rounded-full bg-surface-2 px-3 py-1 text-xs text-grey-2 border border-border/40">
|
|
<span className="text-grey-3">زبان پایه دیتابیس:</span>
|
|
<span className="font-bold text-foreground">
|
|
{SUPPORTED_LANGUAGES.find((l) => l.code === transData.primary_language)?.label ||
|
|
transData.primary_language}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* دکمه ضربدر بالای مودال */}
|
|
<button
|
|
type="button"
|
|
onClick={() => onOpenChange(false)}
|
|
aria-label="بستن پنجره"
|
|
title="بستن"
|
|
className="flex size-8 items-center justify-center rounded-lg text-grey-3 hover:text-foreground hover:bg-white/10 transition-colors shrink-0 cursor-pointer mr-1"
|
|
>
|
|
<Ic name="x" className="size-4.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 5-Language Selector Bar */}
|
|
<div className="flex items-center gap-1.5 mt-4 overflow-x-auto pb-1">
|
|
{SUPPORTED_LANGUAGES.map((lang) => {
|
|
const isSelected = lang.code === selectedLang
|
|
const hasTrans = Boolean(
|
|
transData?.primary_language === lang.code ||
|
|
transData?.translations[lang.code]?.title
|
|
)
|
|
return (
|
|
<button
|
|
key={lang.code}
|
|
type="button"
|
|
onClick={() => handleLanguageChange(lang.code)}
|
|
className={`flex items-center gap-2 rounded-xl px-3.5 py-2 text-xs font-bold transition-all ${
|
|
isSelected
|
|
? 'bg-primary text-white shadow-md'
|
|
: 'bg-surface-2 text-grey-2 hover:bg-surface-3 hover:text-foreground border border-border/30'
|
|
}`}
|
|
>
|
|
<span className="text-sm">{lang.flag}</span>
|
|
<span>{lang.label}</span>
|
|
{hasTrans && !isSelected && (
|
|
<span className="size-1.5 rounded-full bg-emerald-500" />
|
|
)}
|
|
{transData?.primary_language === lang.code && (
|
|
<span className="text-[10px] opacity-80">(اصلی)</span>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</DialogHeader>
|
|
|
|
{/* Dialog Body */}
|
|
<div className="flex-1 overflow-y-auto px-6 py-5 space-y-5">
|
|
{isLoading ? (
|
|
<TourDetailOverviewShimmer />
|
|
) : (
|
|
<>
|
|
{errorMessage && (
|
|
<div className="flex items-center gap-2 rounded-xl bg-rose-500/10 border border-rose-500/20 px-4 py-3 text-xs text-rose-400">
|
|
<Ic name="alert" className="size-4 shrink-0" />
|
|
<span>{errorMessage}</span>
|
|
</div>
|
|
)}
|
|
|
|
{successMessage && (
|
|
<div className="flex items-center gap-2 rounded-xl bg-emerald-500/10 border border-emerald-500/20 px-4 py-3 text-xs text-emerald-400">
|
|
<Ic name="check" className="size-4 shrink-0" />
|
|
<span>{successMessage}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Section Tabs */}
|
|
<div className="flex items-center gap-2 border-b border-border/40 pb-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveSection('base')}
|
|
className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-colors ${
|
|
activeSection === 'base'
|
|
? 'bg-primary/15 text-primary'
|
|
: 'text-grey-3 hover:text-foreground'
|
|
}`}
|
|
>
|
|
اطلاعات کلی (عنوان و شرح)
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveSection('features')}
|
|
className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-colors ${
|
|
activeSection === 'features'
|
|
? 'bg-primary/15 text-primary'
|
|
: 'text-grey-3 hover:text-foreground'
|
|
}`}
|
|
>
|
|
ویژگیهای تور ({formFeatures.length})
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveSection('tips')}
|
|
className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-colors ${
|
|
activeSection === 'tips'
|
|
? 'bg-primary/15 text-primary'
|
|
: 'text-grey-3 hover:text-foreground'
|
|
}`}
|
|
>
|
|
نکات و راهنمای سفر ({formTips.length})
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveSection('itinerary')}
|
|
className={`px-3 py-1.5 text-xs font-bold rounded-lg transition-colors ${
|
|
activeSection === 'itinerary'
|
|
? 'bg-primary/15 text-primary'
|
|
: 'text-grey-3 hover:text-foreground'
|
|
}`}
|
|
>
|
|
مراحل سفر ({itineraries.length})
|
|
</button>
|
|
</div>
|
|
|
|
{/* Active Section Form */}
|
|
<div dir={activeLangConfig?.dir || 'rtl'} className="space-y-4">
|
|
{/* 1. Base Info */}
|
|
{activeSection === 'base' && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-xs font-bold text-grey-2 mb-1.5">
|
|
عنوان تور به زبان {activeLangConfig?.label} ({selectedLang}):
|
|
</label>
|
|
<Input
|
|
value={formTitle}
|
|
onChange={(e) => setFormTitle(e.target.value)}
|
|
placeholder={`عنوان تور به زبان ${activeLangConfig?.label}...`}
|
|
className="bg-surface-2 border-border/50 text-sm"
|
|
disabled={isPrimary}
|
|
/>
|
|
{isPrimary && (
|
|
<p className="text-[11px] text-amber-400/80 mt-1">
|
|
* این زبان، زبان اصلی تور است و از فرم اصلی مدیریت میشود.
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-bold text-grey-2 mb-1.5">
|
|
شرح و توضیحات تور:
|
|
</label>
|
|
<textarea
|
|
rows={4}
|
|
value={formDesc}
|
|
onChange={(e) => setFormDesc(e.target.value)}
|
|
placeholder={`توضیحات کامل تور به زبان ${activeLangConfig?.label}...`}
|
|
disabled={isPrimary}
|
|
className="w-full rounded-xl border border-border/50 bg-surface-2 px-3.5 py-2.5 text-sm text-foreground placeholder:text-grey-4 focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary disabled:opacity-60"
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 2. Tour Features */}
|
|
{activeSection === 'features' && (
|
|
<div className="space-y-3">
|
|
<div className="flex items-center gap-2">
|
|
<Input
|
|
value={newFeatureText}
|
|
onChange={(e) => setNewFeatureText(e.target.value)}
|
|
placeholder={`افزودن ویژگی جدید به ${activeLangConfig?.label}...`}
|
|
className="bg-surface-2 border-border/50 text-sm"
|
|
onKeyDown={(e) => {
|
|
if (e.key === 'Enter') {
|
|
e.preventDefault()
|
|
handleAddFeature()
|
|
}
|
|
}}
|
|
/>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
onClick={handleAddFeature}
|
|
className="shrink-0 bg-primary/20 text-primary hover:bg-primary hover:text-white"
|
|
>
|
|
<Ic name="plus" className="size-4" />
|
|
<span>افزودن</span>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-2 mt-3">
|
|
{formFeatures.length === 0 ? (
|
|
<p className="text-xs text-grey-4 text-center py-6">
|
|
هنوز ویژگی برای این زبان اضافه نشده است.
|
|
</p>
|
|
) : (
|
|
formFeatures.map((feat, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="flex items-center justify-between gap-3 rounded-xl bg-surface-2 border border-border/40 px-3.5 py-2 text-xs"
|
|
>
|
|
<span className="font-medium text-foreground">{feat}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveFeature(idx)}
|
|
className="text-grey-4 hover:text-rose-400 p-1"
|
|
>
|
|
<Ic name="x" className="size-3.5" />
|
|
</button>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 3. Travel Tips */}
|
|
{activeSection === 'tips' && (
|
|
<div className="space-y-3">
|
|
<div className="rounded-xl bg-surface-2/60 border border-border/40 p-3 space-y-2">
|
|
<Input
|
|
value={newTipTitle}
|
|
onChange={(e) => setNewTipTitle(e.target.value)}
|
|
placeholder="عنوان نکته (مثلاً: لباس مناسب / Walking Shoes)..."
|
|
className="bg-surface-2 text-xs"
|
|
/>
|
|
<textarea
|
|
rows={2}
|
|
value={newTipDesc}
|
|
onChange={(e) => setNewTipDesc(e.target.value)}
|
|
placeholder="توضیح نکته سفر..."
|
|
className="w-full rounded-xl border border-border/50 bg-surface-2 px-3 py-2 text-xs text-foreground placeholder:text-grey-4 focus:border-primary focus:outline-none"
|
|
/>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
onClick={handleAddTip}
|
|
className="w-full bg-primary/20 text-primary hover:bg-primary hover:text-white text-xs h-8"
|
|
>
|
|
<Ic name="plus" className="size-3.5" />
|
|
<span>افزودن نکته سفر</span>
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="space-y-2 mt-3">
|
|
{formTips.length === 0 ? (
|
|
<p className="text-xs text-grey-4 text-center py-6">
|
|
هنوز نکته سفری برای این زبان ثبت نشده است.
|
|
</p>
|
|
) : (
|
|
formTips.map((tip, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="rounded-xl bg-surface-2 border border-border/40 p-3 text-xs space-y-1 relative"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-bold text-foreground">{tip.title}</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveTip(idx)}
|
|
className="text-grey-4 hover:text-rose-400 p-0.5"
|
|
>
|
|
<Ic name="x" className="size-3.5" />
|
|
</button>
|
|
</div>
|
|
<p className="text-grey-3 leading-relaxed">{tip.desc}</p>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 4. Itinerary Steps Translations */}
|
|
{activeSection === 'itinerary' && (
|
|
<div className="space-y-4">
|
|
{itineraries.length === 0 ? (
|
|
<p className="text-xs text-grey-4 text-center py-8">
|
|
هیچ مرحله سفری برای این تور ثبت نشده است.
|
|
</p>
|
|
) : (
|
|
itineraries.map((it, idx) => {
|
|
const draft = itinDrafts[it.id] || { title: '', summary: '' }
|
|
return (
|
|
<div
|
|
key={it.id}
|
|
className="rounded-xl bg-surface-2 border border-border/40 p-3.5 space-y-2.5"
|
|
>
|
|
<div className="flex items-center justify-between text-xs">
|
|
<div className="flex items-center gap-2">
|
|
<span className="flex size-5 items-center justify-center rounded-full bg-primary/20 text-primary text-[10px] font-bold">
|
|
{idx + 1}
|
|
</span>
|
|
<span className="font-bold text-foreground">
|
|
عنوان اصلی: {it.title}
|
|
</span>
|
|
</div>
|
|
<span className="text-[10px] text-grey-3">شناسه #{it.id}</span>
|
|
</div>
|
|
|
|
<Input
|
|
value={draft.title}
|
|
onChange={(e) =>
|
|
handleItineraryDraftChange(it.id, 'title', e.target.value)
|
|
}
|
|
placeholder={`عنوان مرحله به ${activeLangConfig?.label}...`}
|
|
className="bg-surface-3 border-border/50 text-xs"
|
|
/>
|
|
|
|
<textarea
|
|
rows={2}
|
|
value={draft.summary}
|
|
onChange={(e) =>
|
|
handleItineraryDraftChange(it.id, 'summary', e.target.value)
|
|
}
|
|
placeholder={`شرح مرحله به ${activeLangConfig?.label}...`}
|
|
className="w-full rounded-xl border border-border/50 bg-surface-3 px-3 py-2 text-xs text-foreground placeholder:text-grey-4 focus:border-primary focus:outline-none"
|
|
/>
|
|
</div>
|
|
)
|
|
})
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Dialog Footer */}
|
|
<DialogFooter className="px-6 py-4 bg-secondary/10 border-t border-border/50 flex items-center justify-between">
|
|
<div className="text-xs text-grey-3">
|
|
زبان در حال ویرایش: <span className="font-bold text-primary">{activeLangConfig?.label}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<DialogClose asChild>
|
|
<Button type="button" variant="outline" size="sm" className="rounded-xl">
|
|
بستن
|
|
</Button>
|
|
</DialogClose>
|
|
<Button
|
|
type="button"
|
|
size="sm"
|
|
disabled={isPrimary || isLoading}
|
|
isLoading={isSaving}
|
|
loadingText="در حال ذخیره..."
|
|
onClick={handleSave}
|
|
className="rounded-xl bg-primary text-white hover:bg-primary/90 gap-1.5 shadow-sm"
|
|
>
|
|
<Ic name="check" className="size-3.5" />
|
|
<span>ذخیره ترجمه {activeLangConfig?.label}</span>
|
|
</Button>
|
|
</div>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|