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:
Alireza Hassani
2026-07-22 13:48:53 +03:30
co-authored by Cursor
commit f566387c61
509 changed files with 62690 additions and 0 deletions
+145
View File
@@ -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>
)
}