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>
This commit is contained in:
co-authored by
Cursor
parent
a11f9643bb
commit
cc94b929e0
@@ -0,0 +1,191 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user