Browse Source

feat: add image, blog slug, and journeys count fields to custom trip destinations

develop
parent
commit
2615d2c4ae
  1. 75
      src/features/custom-trip/CustomTripDestinationDetailPage.tsx
  2. 84
      src/features/custom-trip/components/CustomTripDestinationsTab.tsx
  3. 7
      src/features/custom-trip/services/custom-trip-api.ts
  4. 18
      src/features/custom-trip/types.ts

75
src/features/custom-trip/CustomTripDestinationDetailPage.tsx

@ -89,6 +89,9 @@ export default function CustomTripDestinationDetailPage() {
const [destNameEn, setDestNameEn] = useState('')
const [destCountry, setDestCountry] = useState('عراق')
const [destFlag, setDestFlag] = useState('🇮🇶')
const [destImage, setDestImage] = useState('')
const [destBlogSlug, setDestBlogSlug] = useState('')
const [destJourneysCount, setDestJourneysCount] = useState<number>(10)
const [destDescription, setDestDescription] = useState('')
const [destHolyPlaces, setDestHolyPlaces] = useState('')
const [destIsActive, setDestIsActive] = useState(true)
@ -151,6 +154,9 @@ export default function CustomTripDestinationDetailPage() {
setDestNameEn(target.nameEn || '')
setDestCountry(target.country)
setDestFlag(target.flag || '📍')
setDestImage(target.image || '')
setDestBlogSlug(target.blogSlug || target.blog_slug || '')
setDestJourneysCount(target.journeysCount ?? target.journeys_count ?? 10)
setDestDescription(target.description || '')
setDestHolyPlaces((target.holyPlaces || []).join('، '))
setDestIsActive(target.isActive)
@ -433,6 +439,9 @@ export default function CustomTripDestinationDetailPage() {
nameEn: primaryNameEn,
country: destCountry.trim(),
flag: destFlag,
image: destImage.trim() || undefined,
blogSlug: destBlogSlug.trim() || undefined,
journeysCount: Number(destJourneysCount) || 10,
description: primaryDesc,
holyPlaces: primaryPlaces.length ? primaryPlaces : undefined,
isActive: destIsActive,
@ -884,8 +893,21 @@ export default function CustomTripDestinationDetailPage() {
<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 className="flex items-center gap-4">
{destination.image || destImage ? (
<div className="size-16 sm:size-20 rounded-2xl overflow-hidden shrink-0 border border-border/70 shadow-sm bg-muted/40">
<img
src={destination.image || destImage}
alt={destination.name}
className="size-full object-cover"
onError={(e) => {
(e.currentTarget as HTMLElement).style.display = 'none'
}}
/>
</div>
) : (
<span className="text-3xl select-none">{destFlag || destination.flag || '📍'}</span>
)}
<div>
<div className="flex items-center gap-2.5 flex-wrap">
<h2 className="text-lg sm:text-xl font-black text-foreground">
@ -1085,6 +1107,55 @@ export default function CustomTripDestinationDetailPage() {
{/* فرم: بدنه اسکرول‌شونده و فوتر فیکس‌شده */}
<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-3 gap-3 p-3.5 rounded-xl bg-card border border-border/70 shadow-2xs">
<div className="space-y-1.5 sm:col-span-2">
<label className="text-xs font-bold text-foreground flex items-center justify-between">
<span className="flex items-center gap-1.5">
<Ic name="image" className="size-3.5 text-muted-foreground" />
<span>تصویر شهر مقصد (URL یا مسیر فایل)</span>
</span>
{destImage && (
<span className="text-[10px] text-emerald-600 font-medium">دارای تصویر</span>
)}
</label>
<Input
value={destImage}
onChange={(e) => setDestImage(e.target.value)}
dir="ltr"
placeholder="مثال: /media/destinations/karbala.jpg یا https://..."
className="h-10 rounded-xl text-xs bg-card border-border/70 font-mono shadow-2xs"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-foreground block">
تعداد سفرهای فعال:
</label>
<Input
type="number"
min={0}
value={destJourneysCount}
onChange={(e) => setDestJourneysCount(Number(e.target.value) || 0)}
dir="ltr"
className="h-10 rounded-xl text-xs bg-card border-border/70 font-mono shadow-2xs"
/>
</div>
<div className="space-y-1.5 sm:col-span-3">
<label className="text-xs font-bold text-foreground block">
اسلاگ مقاله راهنمای زیارت در بلاگ (اختیاری):
</label>
<Input
value={destBlogSlug}
onChange={(e) => setDestBlogSlug(e.target.value)}
dir="ltr"
placeholder="مثال: ziyarat-rituals-and-prayers-of-karbala"
className="h-10 rounded-xl text-xs bg-card border-border/70 font-mono shadow-2xs"
/>
</div>
</div>
{/* تنظیمات کلی شهر مقصد (کشور و وضعیت) */}
<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">
{/* انتخاب کشور مقصد */}

84
src/features/custom-trip/components/CustomTripDestinationsTab.tsx

@ -324,6 +324,9 @@ export function CustomTripDestinationsTab({
const [formFlag, setFormFlag] = useState('🇮🇶')
const [selectedCityPresetId, setSelectedCityPresetId] = useState<string>('')
const [formIsActive, setFormIsActive] = useState(true)
const [formImage, setFormImage] = useState<string>('')
const [formBlogSlug, setFormBlogSlug] = useState<string>('')
const [formJourneysCount, setFormJourneysCount] = useState<number>(10)
const currentLang =
SERVICE_LANGUAGES.find((l) => l.code === activeLangTab) || SERVICE_LANGUAGES[0]
@ -360,6 +363,9 @@ export function CustomTripDestinationsTab({
setFormFlag('🇮🇶')
setSelectedCityPresetId('')
setFormIsActive(true)
setFormImage('')
setFormBlogSlug('')
setFormJourneysCount(10)
setValidationError(null)
setIsFormOpen(true)
}
@ -392,6 +398,9 @@ export function CustomTripDestinationsTab({
setDescriptionsMap(initialDesc)
setFormCountry(item.country)
setFormFlag(item.flag || '📍')
setFormImage(item.image || '')
setFormBlogSlug(item.blogSlug || item.blog_slug || '')
setFormJourneysCount(item.journeysCount ?? item.journeys_count ?? 10)
// Find if item corresponds to an official preset city
const matched = OFFICIAL_PRESET_CITIES.find(
@ -473,6 +482,9 @@ export function CustomTripDestinationsTab({
nameEn: names.en.trim() || undefined,
country: formCountry.trim(),
flag: formFlag,
image: formImage.trim() || undefined,
blogSlug: formBlogSlug.trim() || undefined,
journeysCount: Number(formJourneysCount) || 10,
description: descriptionsMap.fa.trim() || descriptionsMap.ar.trim() || undefined,
holyPlaces: holyPlacesArray.length ? holyPlacesArray : undefined,
isActive: formIsActive,
@ -486,6 +498,9 @@ export function CustomTripDestinationsTab({
nameEn: names.en.trim() || undefined,
country: formCountry.trim() || 'عراق',
flag: formFlag,
image: formImage.trim() || undefined,
blogSlug: formBlogSlug.trim() || undefined,
journeysCount: Number(formJourneysCount) || 10,
description: descriptionsMap.fa.trim() || descriptionsMap.ar.trim() || undefined,
holyPlaces: holyPlacesArray.length ? holyPlacesArray : undefined,
hotelsCount: 0,
@ -643,8 +658,21 @@ export function CustomTripDestinationsTab({
>
<td className="px-4 py-3.5 font-mono text-muted-foreground">{toFa(idx + 1)}</td>
<td className="px-4 py-3.5 font-bold text-foreground">
<div className="flex items-center gap-2.5">
<span className="text-xl select-none">{item.flag || '📍'}</span>
<div className="flex items-center gap-3">
{item.image ? (
<div className="size-11 rounded-xl overflow-hidden shrink-0 border border-border/70 bg-muted/40 shadow-2xs">
<img
src={item.image}
alt={item.name}
className="size-full object-cover group-hover:scale-105 transition-transform duration-300"
onError={(e) => {
(e.currentTarget as HTMLElement).style.display = 'none'
}}
/>
</div>
) : (
<span className="text-xl select-none">{item.flag || '📍'}</span>
)}
<div>
<div className="group-hover:text-primary transition-colors flex items-center gap-1.5 font-bold text-sm">
<span>{item.name}</span>
@ -801,6 +829,58 @@ export function CustomTripDestinationsTab({
/>
</div>
{/* فیلدهای تنظیمات رسانه‌ای، تصویر و پیوند به راهنمای بلاگ */}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 p-3.5 rounded-xl bg-card border border-border/70 shadow-2xs">
{/* آدرس یا مسیر تصویر شهر */}
<div className="space-y-1.5 sm:col-span-2">
<label className="text-xs font-bold text-foreground flex items-center justify-between">
<span className="flex items-center gap-1.5">
<Ic name="image" className="size-3.5 text-muted-foreground" />
<span>تصویر شهر مقصد (URL یا مسیر فایل)</span>
</span>
{formImage && (
<span className="text-[10px] text-emerald-600 font-medium">دارای تصویر</span>
)}
</label>
<Input
value={formImage}
onChange={(e) => setFormImage(e.target.value)}
dir="ltr"
placeholder="مثال: /media/destinations/karbala.jpg یا https://..."
className="h-10 rounded-xl text-xs bg-card border-border/70 font-mono shadow-2xs"
/>
</div>
{/* تعداد تورها / سفرها */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-foreground block">
تعداد سفرهای فعال:
</label>
<Input
type="number"
min={0}
value={formJourneysCount}
onChange={(e) => setFormJourneysCount(Number(e.target.value) || 0)}
dir="ltr"
className="h-10 rounded-xl text-xs bg-card border-border/70 font-mono shadow-2xs"
/>
</div>
{/* اسلاگ مقاله بلاگ */}
<div className="space-y-1.5 sm:col-span-3">
<label className="text-xs font-bold text-foreground block">
اسلاگ مقاله راهنمای زیارت در بلاگ (اختیاری):
</label>
<Input
value={formBlogSlug}
onChange={(e) => setFormBlogSlug(e.target.value)}
dir="ltr"
placeholder="مثال: ziyarat-rituals-and-prayers-of-karbala"
className="h-10 rounded-xl text-xs bg-card border-border/70 font-mono shadow-2xs"
/>
</div>
</div>
{/* فیلدهای انتخابی کشور و شهر بر اساس کشور انتخابی (پایین‌تر از تب‌های زبان) */}
<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">
{/* فیلد انتخابی کشور مقصد */}

7
src/features/custom-trip/services/custom-trip-api.ts

@ -451,6 +451,13 @@ export async function saveCustomTripDestination(
name_en: item.nameEn,
country: item.country,
flag: item.flag,
image: item.image,
blog_slug: item.blogSlug || item.blog_slug,
blogSlug: item.blogSlug || item.blog_slug,
journeys_count: item.journeysCount ?? item.journeys_count,
journeysCount: item.journeysCount ?? item.journeys_count,
short_description: item.shortDescription || item.short_description,
shortDescription: item.shortDescription || item.short_description,
description: item.description,
holyPlaces: item.holyPlaces,
holy_places: item.holyPlaces,

18
src/features/custom-trip/types.ts

@ -87,12 +87,28 @@ export interface CustomTripDestinationItem {
nameEn?: string
country: string
flag?: string
image?: string
blogSlug?: string
blog_slug?: string
journeysCount?: number
journeys_count?: number
shortDescription?: string
short_description?: string
description?: string
holyPlaces?: string[]
hotelsCount: number
transportsCount?: number
isActive: boolean
translations?: Record<string, { name?: string; description?: string; holyPlaces?: string[] }>
translations?: Record<
string,
{
name?: string
description?: string
short_description?: string
shortDescription?: string
holyPlaces?: string[]
}
>
}
export interface CustomTripTransportRelationItem {

Loading…
Cancel
Save