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.
 
 
 

314 lines
13 KiB

import React, { useState, useEffect, useMemo } from 'react'
import { Ic } from '@/icons'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ShimmerBox, ShimmerLine } from '@/components/shared/shimmer_frontend'
import { toFa } from '@/lib/utils'
import type {
PilgrimageCityItem,
CategoryItem,
DuaItem,
} from '../types/trip-etiquette'
import {
fetchTripEtiquetteCities,
fetchCategories,
fetchDuas,
} from '../services/trip-etiquette-api'
// نام‌های روان و استاندارد فارسی برای شهرهای زیارتی
const CITY_FA_NAMES: Record<string, string> = {
karbala: 'کربلای معلی',
najaf: 'نجف اشرف',
koufa: 'کوفه',
kufa: 'کوفه',
kazemein: 'کاظمین',
kadhimiya: 'کاظمین',
samara: 'سامرا',
samarra: 'سامرا',
make: 'مکه مکرمه',
mecca: 'مکه مکرمه',
madineh: 'مدینه منوره',
medina: 'مدینه منوره',
mashhad: 'مشهد مقدس',
qom: 'قم مقدسه',
}
const COUNTRY_FLAGS: Record<string, string> = {
IQ: '🇮🇶',
IR: '🇮🇷',
SA: '🇸🇦',
}
const COUNTRY_NAMES_FA: Record<string, string> = {
Iraq: 'عراق',
Iran: 'ایران',
'Saudi Arabia': 'عربستان سعودی',
}
export function CitiesServiceView() {
const [cities, setCities] = useState<PilgrimageCityItem[]>([])
const [categories, setCategories] = useState<CategoryItem[]>([])
const [duas, setDuas] = useState<DuaItem[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isRefreshing, setIsRefreshing] = useState(false)
const [errorMessage, setErrorMessage] = useState<string | null>(null)
// فیلترها
const [searchTerm, setSearchTerm] = useState('')
const [selectedCountryCode, setSelectedCountryCode] = useState('ALL')
const loadData = async (isManual = false) => {
if (isManual) setIsRefreshing(true)
else setIsLoading(true)
setErrorMessage(null)
try {
const [citiesRes, categoriesRes, duasRes] = await Promise.all([
fetchTripEtiquetteCities(),
fetchCategories(),
fetchDuas(),
])
setCities(citiesRes)
setCategories(categoriesRes)
setDuas(duasRes)
} catch (err: any) {
setErrorMessage(err?.message || 'خطا در بارگذاری فهرست شهرهای زیارتی.')
} finally {
setIsLoading(false)
setIsRefreshing(false)
}
}
useEffect(() => {
loadData()
}, [])
// کشورهای موجود در لیست شهرها
const availableCountries = useMemo(() => {
const map = new Map<string, { code: string; name: string; nameFa: string; flag: string }>()
cities.forEach((c) => {
const code = c.country_code || ''
const name = c.country_name || ''
if (code && !map.has(code)) {
map.set(code, {
code,
name,
nameFa: COUNTRY_NAMES_FA[name] || name,
flag: COUNTRY_FLAGS[code] || '🌍',
})
}
})
return Array.from(map.values())
}, [cities])
// شهرهای فیلترشده
const filteredCities = useMemo(() => {
return cities.filter((city) => {
// فیلتر کشور
if (selectedCountryCode !== 'ALL') {
if ((city.country_code || '').toLowerCase() !== selectedCountryCode.toLowerCase()) {
return false
}
}
// فیلتر جستجو بر اساس نام، شناسه یا اسلاگ
if (searchTerm.trim()) {
const term = searchTerm.toLowerCase().trim()
const faName = CITY_FA_NAMES[city.slug.toLowerCase()] || ''
const matchName = city.name.toLowerCase().includes(term)
const matchFa = faName.toLowerCase().includes(term)
const matchSlug = city.slug.toLowerCase().includes(term)
const matchId = String(city.id).includes(term)
return matchName || matchFa || matchSlug || matchId
}
return true
})
}, [cities, selectedCountryCode, searchTerm])
return (
<div className="space-y-6">
{/* سربرگ شهرهای زیارتی به همراه دکمه تازه‌سازی (لود مجدد) */}
<div className="flex items-center justify-between rounded-2xl border border-border-soft bg-card/40 p-4 sm:p-5 shadow-xs backdrop-blur-xs">
<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="map" className="size-5" />
</div>
<h1 className="text-lg sm:text-xl font-black text-foreground">
شهرهای زیارتی
</h1>
</div>
<Button
variant="outline"
size="sm"
onClick={() => loadData(true)}
disabled={isRefreshing}
className="rounded-xl border-border-soft text-xs gap-1.5 h-8.5 px-3 hover:bg-card/80 cursor-pointer"
>
<Ic name="refresh" className={`size-3.5 ${isRefreshing ? 'animate-spin' : ''}`} />
<span>تازه‌سازی</span>
</Button>
</div>
{/* نوار فیلتر و جستجو */}
<div className="rounded-2xl border border-border-soft bg-card/30 p-3.5 flex flex-col sm:flex-row items-center justify-between gap-3 shadow-xs">
<div className="flex items-center flex-wrap gap-2 w-full sm:w-auto">
{/* تب‌های کشورها */}
<button
onClick={() => setSelectedCountryCode('ALL')}
className={`text-xs px-3 py-1.5 rounded-xl font-bold transition ${
selectedCountryCode === 'ALL'
? 'bg-primary text-primary-foreground shadow-xs'
: 'bg-card/80 text-grey-3 hover:text-foreground border border-border-soft'
}`}
>
همه کشورها ({toFa(cities.length)})
</button>
{availableCountries.map((co) => {
const count = cities.filter((c) => c.country_code === co.code).length
return (
<button
key={co.code}
onClick={() => setSelectedCountryCode(co.code)}
className={`text-xs px-3 py-1.5 rounded-xl font-bold transition flex items-center gap-1.5 ${
selectedCountryCode === co.code
? 'bg-primary text-primary-foreground shadow-xs'
: 'bg-card/80 text-grey-3 hover:text-foreground border border-border-soft'
}`}
>
<span>{co.flag}</span>
<span>{co.nameFa}</span>
<span className="text-[10px] font-mono opacity-80">({toFa(count)})</span>
</button>
)
})}
</div>
{/* فیلد جستجوی سریع */}
<div className="relative w-full sm:w-64">
<Ic name="search" className="absolute start-3 top-1/2 -translate-y-1/2 size-3.5 text-grey-3" />
<Input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="جستجوی نام یا شناسه شهر..."
className="ps-9 bg-card/80 border-border-soft rounded-xl text-xs h-9"
/>
{searchTerm && (
<button
onClick={() => setSearchTerm('')}
className="absolute end-2.5 top-1/2 -translate-y-1/2 text-grey-3 hover:text-foreground p-0.5"
>
<Ic name="x" className="size-3" />
</button>
)}
</div>
</div>
{/* پیام خطا در صورت وجود */}
{errorMessage && (
<div className="p-4 rounded-2xl border border-destructive/20 bg-destructive/5 text-destructive text-xs flex items-center justify-between">
<div className="flex items-center gap-2">
<Ic name="alert" className="size-4 shrink-0" />
<span>{errorMessage}</span>
</div>
<Button size="sm" variant="ghost" onClick={() => loadData(true)} className="h-7 text-xs">
تلاش مجدد
</Button>
</div>
)}
{/* وضعیت لودینگ اسکلتون منطبق با ساختار کارت‌های ساده‌شده شهر */}
{isLoading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: 6 }).map((_, i) => (
<div
key={i}
className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-3.5 shadow-xs"
>
{/* عنوان و کشور */}
<div className="flex items-center justify-between">
<ShimmerLine width={110} height={20} rounded="md" />
<ShimmerBox width={65} height={24} rounded="xl" />
</div>
{/* آمار دو ستونه */}
<div className="grid grid-cols-2 gap-2.5 pt-3 border-t border-border-soft/60">
<ShimmerBox height={52} rounded="xl" />
<ShimmerBox height={52} rounded="xl" />
</div>
</div>
))}
</div>
) : filteredCities.length === 0 ? (
<div className="py-16 text-center rounded-2xl border border-dashed border-border-soft bg-card/20 space-y-2">
<Ic name="map" className="size-8 mx-auto text-grey-3" />
<p className="text-sm font-bold text-foreground">هیچ شهری با این مشخصات یافت نشد</p>
</div>
) : (
/* شبکه کارت‌های شهرهای زیارتی (ساده‌شده، فقط خواندنی و بدون واکنش کلیک) */
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredCities.map((city) => {
const faName = CITY_FA_NAMES[city.slug.toLowerCase()] || city.name
const flag = COUNTRY_FLAGS[city.country_code || ''] || '🌍'
const countryFa = COUNTRY_NAMES_FA[city.country_name || ''] || city.country_name || ''
// ۱. تعداد دسته‌بندی‌های متصل به این شهر در دیتابیس
const cityCategoriesCount = categories.filter((c) => {
if (c.city === city.id || c.city_info?.id === city.id) return true
if (c.cities && c.cities.some((ct) => ct.id === city.id)) return true
return false
}).length
// ۲. تعداد کل ادعیه متصل به این شهر در دیتابیس
const cityDuasCount = duas.filter((d) => {
if (d.cities && d.cities.some((c) => c.id === city.id)) return true
if (d.categories && d.categories.some((cat) => {
if (cat.city_id === city.id) return true
if (cat.cities && cat.cities.some((c) => c.id === city.id)) return true
return false
})) return true
if (d.shrines && d.shrines.some((s: any) => s.city_id === city.id || s.city === city.id)) return true
return false
}).length
return (
<div
key={city.id}
className="rounded-2xl border border-border-soft bg-card/40 p-4 space-y-3.5 shadow-xs select-none cursor-default"
>
{/* بخش اول: نشان کشور به همراه پرچم (سمت چپ) و عنوان شهر (سمت راست) */}
<div className="flex items-center justify-between">
<h3 className="text-base sm:text-lg font-black text-foreground">
{faName}
</h3>
<div className="flex items-center gap-1.5 text-xs text-grey-2 bg-card/80 border border-border-soft/60 px-2.5 py-1 rounded-xl font-medium">
<span>{countryFa}</span>
<span className="text-sm leading-none">{flag}</span>
</div>
</div>
{/* بخش دوم: دو آمار تکمیلی صرفاً نمایشی */}
<div className="grid grid-cols-2 gap-2.5 pt-3 border-t border-border-soft/60">
<div className="rounded-xl border border-border-soft/60 bg-card/60 p-2.5 flex flex-col items-center justify-center text-center">
<span className="text-[11px] text-grey-3 font-medium">دسته‌بندی‌ها</span>
<span className="text-sm font-black text-foreground font-mono mt-0.5 tabular-nums">
{toFa(cityCategoriesCount)}
</span>
</div>
<div className="rounded-xl border border-border-soft/60 bg-card/60 p-2.5 flex flex-col items-center justify-center text-center">
<span className="text-[11px] text-grey-3 font-medium">کل ادعیه و زیارات</span>
<span className="text-sm font-black text-foreground font-mono mt-0.5 tabular-nums">
{toFa(cityDuasCount)}
</span>
</div>
</div>
</div>
)
})}
</div>
)}
</div>
)
}