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
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>
|
|
)
|
|
}
|