mirror of
https://git.meshkee.com/Meshkee/dashboards.git
synced 2026-08-11 22:30:58 +04:30
Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { CalendarDays, User } from 'lucide-react'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { BlogCommentsSection } from '../components/BlogCommentsSection'
|
||||
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 pageStyles from '../components/PageContent.module.css'
|
||||
import styles from './BlogDetailsPage.module.css'
|
||||
|
||||
export function BlogDetailsPage() {
|
||||
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('Unable to load blog post.')
|
||||
}
|
||||
setBlog(null)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void loadBlog()
|
||||
return () => controller.abort()
|
||||
}, [id])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<p className={styles.status}>Loading blog post...</p>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !blog || !id) {
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<p className={styles.error}>{error || 'Blog post not found.'}</p>
|
||||
<Link to="/blog/list" className={styles.backLink}>
|
||||
Back to My Blogs
|
||||
</Link>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
const displayDate = formatBlogDate(blog.publishedAt ?? blog.createdAt)
|
||||
const typeLabel =
|
||||
BLOG_TYPE_OPTIONS.find((option) => option.value === blog.type)?.label ?? blog.type
|
||||
|
||||
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}>{blog.title}</h2>
|
||||
{blog.abstract && <p className={pageStyles.pageSubtitle}>{blog.abstract}</p>}
|
||||
</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}
|
||||
dangerouslySetInnerHTML={{ __html: blog.mainTextHtml }}
|
||||
/>
|
||||
) : (
|
||||
<p className={styles.content}>No content yet.</p>
|
||||
)}
|
||||
|
||||
<div className={styles.authorRow}>
|
||||
<User size={16} />
|
||||
<span className={styles.authorLabel}>Author</span>
|
||||
<span className={styles.authorName}>{formatBlogAuthor(blog.author)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BlogCommentsSection blogId={id} />
|
||||
</article>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user