144 lines
3.8 KiB
TypeScript
144 lines
3.8 KiB
TypeScript
"use client"
|
||
|
||
import type { IProjectCardProps } from "./ProjectCard.d"
|
||
import type { JSX } from "react"
|
||
|
||
import { Image as ImageIcon, MoreHorizontal } from "lucide-react"
|
||
import { FunctionComponent } from "react"
|
||
|
||
import cs from "classnames"
|
||
|
||
import { formatRelativeTime } from "@shared/lib/dates"
|
||
import { Card } from "@shared/ui/Card"
|
||
import { CircularProgress } from "@shared/ui/CircularProgress"
|
||
import {
|
||
Dropdown,
|
||
DropdownContent,
|
||
DropdownItem,
|
||
DropdownTrigger,
|
||
} from "@shared/ui/Dropdown"
|
||
|
||
import styles from "./ProjectCard.module.scss"
|
||
|
||
export const ProjectCard: FunctionComponent<IProjectCardProps> = ({
|
||
project,
|
||
className,
|
||
progress = 0,
|
||
currentAction,
|
||
imageUrl,
|
||
onClick,
|
||
onEdit,
|
||
onRename,
|
||
onDelete,
|
||
}): JSX.Element => {
|
||
const { name, updated_at, status } = project
|
||
|
||
const temporaryStatuses = new Set(["PROCESSING", "RENDERING", "UPLOADING"])
|
||
const isCompleted = status === "DONE"
|
||
const isProcessing = temporaryStatuses.has(status)
|
||
const isDraft = status === "DRAFT"
|
||
const isFailed = status === "FAILED"
|
||
|
||
const shouldShowProgress = isProcessing
|
||
|
||
const getStatusClass = () => {
|
||
if (isCompleted) return styles.statusGenerated
|
||
if (isProcessing) return styles.statusProcessing
|
||
if (isDraft) return styles.statusDraft
|
||
if (isFailed) return styles.statusFailed
|
||
return styles.statusDraft
|
||
}
|
||
|
||
const getStatusLabel = () => {
|
||
if (isCompleted) return "Завершено"
|
||
if (isProcessing) return "В процессе"
|
||
if (isDraft) return "Черновик"
|
||
if (isFailed) return "Ошибка"
|
||
return status
|
||
}
|
||
|
||
const displayAction = currentAction || (isProcessing ? "В процессе" : "")
|
||
|
||
return (
|
||
<Card className={cs(styles.root, className)} onClick={onClick}>
|
||
<div className={styles.hero}>
|
||
{imageUrl ? (
|
||
<img src={imageUrl} alt={name} loading="lazy" />
|
||
) : (
|
||
<div
|
||
className={styles.placeholder}
|
||
data-color-index={name.charCodeAt(0) % 4}
|
||
>
|
||
<ImageIcon />
|
||
</div>
|
||
)}
|
||
|
||
<div className={styles.statusBadge}>
|
||
<div className={cs(styles.status, getStatusClass())}>
|
||
<span className={styles.statusDot} />
|
||
{getStatusLabel()}
|
||
</div>
|
||
</div>
|
||
|
||
{shouldShowProgress && (
|
||
<div className={styles.progressOverlay}>
|
||
<div className={styles.progressCircle}>
|
||
<CircularProgress
|
||
percentage={Math.min(100, Math.max(0, progress))}
|
||
color="var(--purple-500)"
|
||
bgClassName={styles.progressBg}
|
||
valueClassName={styles.progressValue}
|
||
/>
|
||
<span className={styles.percentage}>{Math.round(progress)}%</span>
|
||
</div>
|
||
{displayAction && (
|
||
<span className={styles.actionName} title={displayAction}>
|
||
{displayAction}
|
||
</span>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
<div className={styles.content}>
|
||
<div className={styles.info}>
|
||
<div className={styles.infoHeader}>
|
||
<h3 className={styles.title} title={name}>
|
||
{name}
|
||
</h3>
|
||
<div
|
||
className={styles.menuTrigger}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<Dropdown>
|
||
<DropdownTrigger asChild>
|
||
<button type="button" aria-label="Действия проекта">
|
||
<MoreHorizontal size={16} />
|
||
</button>
|
||
</DropdownTrigger>
|
||
<DropdownContent align="end">
|
||
<DropdownItem onSelect={() => onEdit?.()}>
|
||
Изменить
|
||
</DropdownItem>
|
||
<DropdownItem onSelect={() => onRename?.()}>
|
||
Переименовать
|
||
</DropdownItem>
|
||
<DropdownItem
|
||
className="text-red-500"
|
||
onSelect={() => onDelete?.()}
|
||
>
|
||
Удалить
|
||
</DropdownItem>
|
||
</DropdownContent>
|
||
</Dropdown>
|
||
</div>
|
||
</div>
|
||
<span className={styles.date}>
|
||
Создано {formatRelativeTime(updated_at)}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
)
|
||
}
|