108 lines
3.7 KiB
TypeScript
108 lines
3.7 KiB
TypeScript
|
|
import { useState, useEffect } from 'react'
|
|||
|
|
import { useParams, useLocation, Link } from 'react-router-dom'
|
|||
|
|
import { contentApi } from '@/api/admin'
|
|||
|
|
import type { StaticContent } from '@/types'
|
|||
|
|
import { GlassCard } from '@/components/ui'
|
|||
|
|
import { ArrowLeft, Loader2, FileText } from 'lucide-react'
|
|||
|
|
|
|||
|
|
// Map routes to content keys
|
|||
|
|
const ROUTE_KEY_MAP: Record<string, string> = {
|
|||
|
|
'/terms': 'terms_of_service',
|
|||
|
|
'/privacy': 'privacy_policy',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function StaticContentPage() {
|
|||
|
|
const { key: paramKey } = useParams<{ key: string }>()
|
|||
|
|
const location = useLocation()
|
|||
|
|
const [content, setContent] = useState<StaticContent | null>(null)
|
|||
|
|
const [isLoading, setIsLoading] = useState(true)
|
|||
|
|
const [error, setError] = useState<string | null>(null)
|
|||
|
|
|
|||
|
|
// Determine content key from route or param
|
|||
|
|
const contentKey = ROUTE_KEY_MAP[location.pathname] || paramKey
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!contentKey) return
|
|||
|
|
|
|||
|
|
const loadContent = async () => {
|
|||
|
|
setIsLoading(true)
|
|||
|
|
setError(null)
|
|||
|
|
try {
|
|||
|
|
const data = await contentApi.getPublicContent(contentKey)
|
|||
|
|
setContent(data)
|
|||
|
|
} catch {
|
|||
|
|
setError('Контент не найден')
|
|||
|
|
} finally {
|
|||
|
|
setIsLoading(false)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
loadContent()
|
|||
|
|
}, [contentKey])
|
|||
|
|
|
|||
|
|
if (isLoading) {
|
|||
|
|
return (
|
|||
|
|
<div className="flex flex-col items-center justify-center py-24">
|
|||
|
|
<Loader2 className="w-12 h-12 animate-spin text-neon-500 mb-4" />
|
|||
|
|
<p className="text-gray-400">Загрузка...</p>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (error || !content) {
|
|||
|
|
return (
|
|||
|
|
<div className="max-w-3xl mx-auto">
|
|||
|
|
<GlassCard className="text-center py-16">
|
|||
|
|
<div className="w-20 h-20 mx-auto mb-6 rounded-2xl bg-dark-700 flex items-center justify-center">
|
|||
|
|
<FileText className="w-10 h-10 text-gray-600" />
|
|||
|
|
</div>
|
|||
|
|
<h3 className="text-xl font-semibold text-white mb-2">Страница не найдена</h3>
|
|||
|
|
<p className="text-gray-400 mb-6">Запрашиваемый контент не существует</p>
|
|||
|
|
<Link
|
|||
|
|
to="/"
|
|||
|
|
className="inline-flex items-center gap-2 text-neon-400 hover:text-neon-300 transition-colors"
|
|||
|
|
>
|
|||
|
|
<ArrowLeft className="w-4 h-4" />
|
|||
|
|
На главную
|
|||
|
|
</Link>
|
|||
|
|
</GlassCard>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="max-w-3xl mx-auto">
|
|||
|
|
<Link
|
|||
|
|
to="/"
|
|||
|
|
className="inline-flex items-center gap-2 text-gray-400 hover:text-neon-400 mb-6 transition-colors group"
|
|||
|
|
>
|
|||
|
|
<ArrowLeft className="w-4 h-4 group-hover:-translate-x-1 transition-transform" />
|
|||
|
|
На главную
|
|||
|
|
</Link>
|
|||
|
|
|
|||
|
|
<GlassCard>
|
|||
|
|
<h1 className="text-2xl md:text-3xl font-bold text-white mb-6">{content.title}</h1>
|
|||
|
|
<div
|
|||
|
|
className="prose prose-invert prose-gray max-w-none
|
|||
|
|
prose-headings:text-white prose-headings:font-semibold
|
|||
|
|
prose-p:text-gray-300 prose-p:leading-relaxed
|
|||
|
|
prose-a:text-neon-400 prose-a:no-underline hover:prose-a:text-neon-300
|
|||
|
|
prose-strong:text-white
|
|||
|
|
prose-ul:text-gray-300 prose-ol:text-gray-300
|
|||
|
|
prose-li:marker:text-gray-500
|
|||
|
|
prose-hr:border-dark-600 prose-hr:my-6
|
|||
|
|
prose-img:rounded-xl prose-img:shadow-lg"
|
|||
|
|
dangerouslySetInnerHTML={{ __html: content.content }}
|
|||
|
|
/>
|
|||
|
|
<div className="mt-8 pt-6 border-t border-dark-600 text-sm text-gray-500">
|
|||
|
|
Последнее обновление: {new Date(content.updated_at).toLocaleDateString('ru-RU', {
|
|||
|
|
day: 'numeric',
|
|||
|
|
month: 'long',
|
|||
|
|
year: 'numeric'
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</GlassCard>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
}
|