Files
dashboards/apps/business/src/pages/InstructionDetailsPage.tsx
T
Alireza HassaniandCursor cc94b929e0 Add e-payment UI, checkout payment result, and legacy migrate domain lookup.
Wire Mellat settings in business dashboard, customer bank return page with payment details, and super-admin migrate modal domain confirmation.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 09:51:22 +03:30

192 lines
5.9 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 { InstructionCommentsSection } from '../components/InstructionCommentsSection'
import { useT } from '../i18n/useT'
import { ApiError } from '../lib/api'
import {
formatInstructionAuthor,
formatInstructionDate,
getInstructionDetail,
resolveInstructionTitle,
} from '../services/instructionService'
import type { InstructionDetail } from '../types/instruction'
import { textLocaleAttrs } from '../utils/textLocale'
import pageStyles from '../components/PageContent.module.css'
import styles from './BlogDetailsPage.module.css'
export function InstructionDetailsPage() {
const t = useT()
const { locale } = useLocale()
const { id } = useParams()
const [video, setInstruction] = useState<InstructionDetail | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState('')
useEffect(() => {
if (!id) return
const controller = new AbortController()
async function loadVideo() {
setIsLoading(true)
setError('')
try {
const data = await getInstructionDetail(id!, controller.signal)
setInstruction(data)
} catch (err) {
if (err instanceof DOMException && err.name === 'AbortError') return
if (err instanceof ApiError) {
setError(err.message)
} else {
setError(t('instruction.details.errorLoad'))
}
setInstruction(null)
} finally {
setIsLoading(false)
}
}
void loadVideo()
return () => controller.abort()
}, [id])
if (isLoading) {
return (
<main className={pageStyles.content}>
<p className={styles.status}>{t('instruction.details.loading')}</p>
</main>
)
}
if (error || !video || !id) {
return (
<main className={pageStyles.content}>
<p className={styles.error}>{error || t('instruction.details.notFound')}</p>
<Link to="/instructions/list" className={styles.backLink}>
{t('instruction.details.back')}
</Link>
</main>
)
}
const displayTitle = resolveInstructionTitle(video, locale === 'fa' ? 'fa' : 'en')
const displayDate = formatInstructionDate(
video.publishedAt ?? video.createdAt,
locale === 'fa' ? 'fa' : 'en',
)
const titleLocale = textLocaleAttrs(displayTitle)
const abstractLocale = textLocaleAttrs(video.abstract)
const contentLocale = textLocaleAttrs(
video.descriptionHtml?.replace(/<[^>]+>/g, ' ') ?? '',
)
const providerLabel =
video.videoProvider === 'youtube' ? 'YouTube' : 'Aparat'
return (
<main className={pageStyles.content}>
<Breadcrumbs
items={[
{ label: 'Dashboard', href: '/' },
{ label: 'Instructions', href: '/instructions' },
{ label: 'My Instructions', href: '/instructions/list' },
{ label: displayTitle },
]}
/>
<div className={pageStyles.pageHeader}>
<div>
<h2
className={[pageStyles.pageTitle, titleLocale.className]
.filter(Boolean)
.join(' ')}
lang={titleLocale.lang}
dir={titleLocale.dir}
>
{displayTitle}
</h2>
{video.abstract ? (
<p
className={[pageStyles.pageSubtitleContent, abstractLocale.className]
.filter(Boolean)
.join(' ')}
lang={abstractLocale.lang}
dir={abstractLocale.dir}
>
{video.abstract}
</p>
) : null}
</div>
</div>
<article className={styles.blogDetail}>
<div className={styles.heroImageWrap}>
{video.coverImageUrl ? (
<img
src={video.coverImageUrl}
alt={displayTitle}
className={styles.heroImage}
/>
) : (
<div className={styles.heroPlaceholder} aria-hidden="true" />
)}
</div>
<div className={styles.metaRow}>
<span className={styles.metaItem}>
<CalendarDays size={15} />
{displayDate}
</span>
{(providerLabel || video.categoryName) && (
<>
<span className={styles.metaDot}>·</span>
<div className={styles.metaChips}>
{providerLabel && (
<span className={styles.typeChip}>{providerLabel}</span>
)}
{video.categoryName && (
<span className={styles.categoryChip}>{video.categoryName}</span>
)}
</div>
</>
)}
</div>
{video.embedCode ? (
<div
className={styles.contentPanel}
dir="ltr"
dangerouslySetInnerHTML={{ __html: video.embedCode }}
/>
) : null}
<div className={styles.contentPanel}>
{video.descriptionHtml ? (
<div
className={[styles.content, contentLocale.className].filter(Boolean).join(' ')}
lang={contentLocale.lang}
dir={contentLocale.dir}
dangerouslySetInnerHTML={{ __html: video.descriptionHtml }}
/>
) : (
<p className={styles.content} lang="en" dir="ltr">
{t('instruction.details.noDescription')}
</p>
)}
<div className={styles.authorRow}>
<User size={16} />
<span className={styles.authorLabel}>{t('instruction.details.author')}</span>
<span className={styles.authorName}>{formatInstructionAuthor(video.author)}</span>
</div>
</div>
<InstructionCommentsSection instructionId={id} />
</article>
</main>
)
}