mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
RTL carousels/FABs, special key+title fields, slider gallery fixes, and dashboard SSL sync agent for super-admin. Co-authored-by: Cursor <cursoragent@cursor.com>
189 lines
5.5 KiB
TypeScript
189 lines
5.5 KiB
TypeScript
import { useEffect, useState } from 'react'
|
|
import { Link, useParams } from 'react-router-dom'
|
|
import { CalendarDays, User } from 'lucide-react'
|
|
import { useLocale } from '@meshkee/dashboard-ui'
|
|
import { Breadcrumbs } from '../components/Breadcrumbs'
|
|
import { BlogCommentsSection } from '../components/BlogCommentsSection'
|
|
import { useT } from '../i18n/useT'
|
|
import { ApiError } from '../lib/api'
|
|
import {
|
|
formatBlogAuthor,
|
|
formatBlogDate,
|
|
getBlogDetail,
|
|
} from '../services/blogService'
|
|
import type { BlogDetail } from '../types/blog'
|
|
import { BLOG_TYPE_OPTIONS } from '../types/blog'
|
|
import { textLocaleAttrs } from '../utils/textLocale'
|
|
import pageStyles from '../components/PageContent.module.css'
|
|
import styles from './BlogDetailsPage.module.css'
|
|
|
|
export function BlogDetailsPage() {
|
|
const t = useT()
|
|
const { locale } = useLocale()
|
|
const { id } = useParams()
|
|
const [blog, setBlog] = useState<BlogDetail | null>(null)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
|
|
useEffect(() => {
|
|
if (!id) return
|
|
|
|
const controller = new AbortController()
|
|
|
|
async function loadBlog() {
|
|
setIsLoading(true)
|
|
setError('')
|
|
|
|
try {
|
|
const data = await getBlogDetail(id!, controller.signal)
|
|
setBlog(data)
|
|
} catch (err) {
|
|
if (err instanceof DOMException && err.name === 'AbortError') return
|
|
if (err instanceof ApiError) {
|
|
setError(err.message)
|
|
} else {
|
|
setError(t('blog.details.errorLoad'))
|
|
}
|
|
setBlog(null)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
|
|
void loadBlog()
|
|
return () => controller.abort()
|
|
}, [id])
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<main className={pageStyles.content}>
|
|
<p className={styles.status}>{t('blog.details.loading')}</p>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
if (error || !blog || !id) {
|
|
return (
|
|
<main className={pageStyles.content}>
|
|
<p className={styles.error}>{error || t('blog.details.notFound')}</p>
|
|
<Link to="/blog/list" className={styles.backLink}>
|
|
{t('blog.details.back')}
|
|
</Link>
|
|
</main>
|
|
)
|
|
}
|
|
|
|
const displayDate = formatBlogDate(
|
|
blog.publishedAt ?? blog.createdAt,
|
|
locale === 'fa' ? 'fa' : 'en',
|
|
)
|
|
const typeValue = BLOG_TYPE_OPTIONS.find((option) => option.value === blog.type)?.value
|
|
const typeLabel = typeValue
|
|
? t(
|
|
typeValue === 'news'
|
|
? 'blog.form.type.news'
|
|
: typeValue === 'article'
|
|
? 'blog.form.type.article'
|
|
: 'blog.form.type.blog',
|
|
)
|
|
: blog.type
|
|
const titleLocale = textLocaleAttrs(blog.title)
|
|
const abstractLocale = textLocaleAttrs(blog.abstract)
|
|
const contentLocale = textLocaleAttrs(
|
|
blog.mainTextHtml?.replace(/<[^>]+>/g, ' ') ?? '',
|
|
)
|
|
|
|
return (
|
|
<main className={pageStyles.content}>
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: 'Dashboard', href: '/' },
|
|
{ label: 'Blog', href: '/blog' },
|
|
{ label: 'My Blogs', href: '/blog/list' },
|
|
{ label: blog.title },
|
|
]}
|
|
/>
|
|
|
|
<div className={pageStyles.pageHeader}>
|
|
<div>
|
|
<h2
|
|
className={[pageStyles.pageTitle, titleLocale.className]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
lang={titleLocale.lang}
|
|
dir={titleLocale.dir}
|
|
>
|
|
{blog.title}
|
|
</h2>
|
|
{blog.abstract ? (
|
|
<p
|
|
className={[pageStyles.pageSubtitle, abstractLocale.className]
|
|
.filter(Boolean)
|
|
.join(' ')}
|
|
lang={abstractLocale.lang}
|
|
dir={abstractLocale.dir}
|
|
>
|
|
{blog.abstract}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
<article className={styles.blogDetail}>
|
|
<div className={styles.heroImageWrap}>
|
|
{blog.titleImageUrl ? (
|
|
<img
|
|
src={blog.titleImageUrl}
|
|
alt={blog.title}
|
|
className={styles.heroImage}
|
|
/>
|
|
) : (
|
|
<div className={styles.heroPlaceholder} aria-hidden="true" />
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.metaRow}>
|
|
<span className={styles.metaItem}>
|
|
<CalendarDays size={15} />
|
|
{displayDate}
|
|
</span>
|
|
{(typeLabel || blog.categoryName) && (
|
|
<>
|
|
<span className={styles.metaDot}>·</span>
|
|
<div className={styles.metaChips}>
|
|
{typeLabel && <span className={styles.typeChip}>{typeLabel}</span>}
|
|
{blog.categoryName && (
|
|
<span className={styles.categoryChip}>{blog.categoryName}</span>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className={styles.contentPanel}>
|
|
{blog.mainTextHtml ? (
|
|
<div
|
|
className={[styles.content, contentLocale.className].filter(Boolean).join(' ')}
|
|
lang={contentLocale.lang}
|
|
dir={contentLocale.dir}
|
|
dangerouslySetInnerHTML={{ __html: blog.mainTextHtml }}
|
|
/>
|
|
) : (
|
|
<p className={styles.content} lang="en" dir="ltr">
|
|
{t('blog.details.noContent')}
|
|
</p>
|
|
)}
|
|
|
|
<div className={styles.authorRow}>
|
|
<User size={16} />
|
|
<span className={styles.authorLabel}>{t('blog.details.author')}</span>
|
|
<span className={styles.authorName}>{formatBlogAuthor(blog.author)}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<BlogCommentsSection blogId={id} />
|
|
</article>
|
|
</main>
|
|
)
|
|
}
|