Files
dashboards/apps/business/src/components/PortfolioCard.tsx
T

179 lines
5.7 KiB
TypeScript

import { Link } from 'react-router-dom'
import { ChevronUp, Link2, Pencil, MessageSquare, Trash2 } from 'lucide-react'
import { useLocale } from '@meshkee/dashboard-ui'
import type { Portfolio } from '../types/portfolio'
import {
formatPortfolioCardDate,
resolvePortfolioExternalUrl,
} from '../services/portfolioService'
import { useT } from '../i18n/useT'
import { textLocaleAttrs } from '../utils/textLocale'
import { buildWebsiteContentUrl } from '../utils/websiteLinks'
import { OpenOnWebsiteControl } from './OpenOnWebsiteControl'
import { Tooltip } from './Tooltip'
import cardStyles from './PortfolioCard.module.css'
import controlStyles from './ProductCard.module.css'
interface PortfolioCardProps {
portfolio: Portfolio
commentCount: number
canMoveUp: boolean
isMovingUp?: boolean
onMoveUp: (id: string) => void
onEdit: (id: string) => void
onComments: (id: string) => void
onRemove: (id: string) => void
}
export function PortfolioCard({
portfolio,
commentCount,
canMoveUp,
isMovingUp = false,
onMoveUp,
onEdit,
onComments,
onRemove,
}: PortfolioCardProps) {
const t = useT()
const { locale } = useLocale()
const isFa = locale === 'fa'
const publishDate = formatPortfolioCardDate(
portfolio.publishedAt ?? portfolio.createdAt,
isFa ? 'fa' : 'en',
)
const titleFa = (portfolio.titleFa || portfolio.title || '').trim()
const titleEn = (portfolio.titleEn || '').trim()
const showBothTitles = Boolean(titleFa && titleEn)
const showAbstract = !showBothTitles
const abstractLocale = textLocaleAttrs(portfolio.abstract)
const hasAbstract = Boolean(portfolio.abstract?.trim())
// In FA UI, keep cards RTL even when EN titles/abstracts are shown.
const abstractDir = isFa ? 'rtl' : abstractLocale.dir
const externalUrl = resolvePortfolioExternalUrl(portfolio.projectUrl)
const websiteUrl = buildWebsiteContentUrl('portfolio', portfolio)
return (
<article className={cardStyles.card}>
<Link to={`/portfolios/detail/${portfolio.id}`} className={cardStyles.clickable}>
<div className={cardStyles.imageWrap}>
{portfolio.titleImageUrl ? (
<img
src={portfolio.titleImageUrl}
alt={titleFa || titleEn || 'Portfolio'}
className={cardStyles.image}
loading="lazy"
/>
) : (
<div className={cardStyles.imagePlaceholder} aria-hidden="true" />
)}
</div>
<div className={cardStyles.body} dir={isFa ? 'rtl' : 'ltr'}>
{titleFa ? (
<h3 className={`${cardStyles.titleFa} faText`} lang="fa" dir="rtl">
{titleFa}
</h3>
) : null}
{titleEn ? (
<p className={cardStyles.titleEn} lang="en" dir="ltr">
{titleEn}
</p>
) : null}
{showAbstract ? (
hasAbstract ? (
<p
className={[cardStyles.abstract, abstractLocale.className]
.filter(Boolean)
.join(' ')}
lang={abstractLocale.lang}
dir={abstractDir}
>
{portfolio.abstract}
</p>
) : (
<p
className={cardStyles.abstract}
lang={isFa ? 'fa' : 'en'}
dir={isFa ? 'rtl' : 'ltr'}
>
{t('portfolio.card.noSummary')}
</p>
)
) : null}
<div className={cardStyles.metaRow} dir="ltr">
{portfolio.categoryName ? (
<span className={cardStyles.categoryChip} dir="auto">
{portfolio.categoryName}
</span>
) : (
<span />
)}
<span className={cardStyles.metaDate} dir="auto">
{publishDate}
</span>
</div>
</div>
</Link>
<div className={controlStyles.controls}>
<Tooltip label={t('portfolio.card.moveUp')}>
<button
type="button"
onClick={() => onMoveUp(portfolio.id)}
disabled={!canMoveUp || isMovingUp}
aria-label={t('portfolio.card.moveUp')}
>
<ChevronUp size={16} />
</button>
</Tooltip>
<Tooltip label={t('portfolio.card.edit')}>
<button
type="button"
onClick={() => onEdit(portfolio.id)}
aria-label={t('portfolio.card.edit')}
>
<Pencil size={16} />
</button>
</Tooltip>
<OpenOnWebsiteControl href={websiteUrl} />
{externalUrl ? (
<Tooltip label={externalUrl}>
<a
href={externalUrl}
target="_blank"
rel="noopener noreferrer"
aria-label={t('portfolio.card.openLink')}
>
<Link2 size={16} />
</a>
</Tooltip>
) : null}
<Tooltip label={t('portfolio.card.comments')}>
<button
type="button"
className={controlStyles.iconBtn}
onClick={() => onComments(portfolio.id)}
aria-label={`${t('portfolio.card.comments')} (${commentCount})`}
>
<MessageSquare size={16} />
{commentCount > 0 && (
<span className={controlStyles.commentBadge}>{commentCount}</span>
)}
</button>
</Tooltip>
<Tooltip label={t('portfolio.card.remove')}>
<button
type="button"
className={controlStyles.danger}
onClick={() => onRemove(portfolio.id)}
aria-label={t('portfolio.card.remove')}
>
<Trash2 size={16} />
</button>
</Tooltip>
</div>
</article>
)
}