Corp/frontend/src/components/feed/FeedPostCard.tsx

216 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Image from "next/image";
import Link from "next/link";
import type { ReactNode } from "react";
import { Heart, MessageSquare, Paperclip, Pin } from "lucide-react";
import { RequestAvatar } from "@/components/requests/RequestAvatar";
import { resolveMediaUrl } from "@/lib/url";
import type { Post } from "@/types/api";
type FeedPostCardProps = {
authorHref?: string | null;
post: Post;
authorSubtitle: ReactNode;
attachmentButtonClassName?: string;
commentsButtonClassName?: string;
footerAction?: ReactNode;
headerActions?: ReactNode;
imageSrc?: string;
isLikeActive?: boolean;
likeButtonClassName?: string;
likeDisabled?: boolean;
likesPopover?: ReactNode;
likesWrapperProps?: {
onMouseEnter?: () => void;
onMouseLeave?: () => void;
};
onLikeToggle?: (post: Post) => void;
onOpenComments: (post: Post) => void;
pinMarkerTitle?: string;
pinnedStyle?: "inline" | "badge" | "none";
};
function formatAuthorName(post: Post) {
const firstName = post.author?.first_name || "";
const lastName = post.author?.last_name || "";
const fullName = `${lastName} ${firstName}`.trim();
return fullName || post.author?.email || "Сотрудник";
}
function formatAuthorFallback(post: Post) {
return (
post.author?.last_name?.[0] ||
post.author?.first_name?.[0] ||
post.author?.email?.[0] ||
"П"
).toUpperCase();
}
export function FeedPostCard({
authorHref,
post,
authorSubtitle,
attachmentButtonClassName,
commentsButtonClassName = "app-action-ghost flex items-center gap-2 rounded-lg px-3 py-2",
footerAction,
headerActions,
imageSrc,
isLikeActive = false,
likeButtonClassName,
likeDisabled = false,
likesPopover,
likesWrapperProps,
onLikeToggle,
onOpenComments,
pinMarkerTitle = "Закрепленная публикация",
pinnedStyle = "inline",
}: FeedPostCardProps) {
const authorName = formatAuthorName(post);
const authorFallback = formatAuthorFallback(post);
const postText = (post.body || post.content || "").trim();
const resolvedImageSrc = resolveMediaUrl(imageSrc || post.image);
const attachmentHref = resolveMediaUrl(post.attachment || post.attachment_url);
const resolvedLikeButtonClassName =
likeButtonClassName ||
`flex items-center gap-2 rounded-lg px-3 py-2 transition ${
isLikeActive
? "app-action-ghost text-[var(--accent-primary)]"
: "app-action-ghost"
}`;
return (
<article id={`post-${post.id}`} className="app-surface rounded-2xl p-5">
<header className="mb-3 flex items-start justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<RequestAvatar
alt={authorName}
fallback={authorFallback}
size="lg"
src={post.author?.avatar}
/>
<div className="min-w-0">
{authorHref ? (
<Link
href={authorHref}
className="block truncate text-sm font-semibold text-[var(--foreground)] transition hover:text-[var(--accent-primary-strong)]"
>
{authorName}
</Link>
) : (
<p className="truncate text-sm font-semibold text-[var(--foreground)]">
{authorName}
</p>
)}
<div className="app-text-muted flex items-center gap-1.5 text-xs">
{authorSubtitle}
{pinnedStyle === "inline" && post.pinned ? (
<span
className="app-accent-text inline-flex items-center justify-center"
title={pinMarkerTitle}
aria-label={pinMarkerTitle}
>
<Pin size={12} className="shrink-0 fill-current" />
</span>
) : null}
</div>
</div>
</div>
<div className="flex shrink-0 items-start gap-2">
{pinnedStyle === "badge" && post.pinned ? (
<span className="app-badge inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium">
<Pin size={12} className="app-accent-text fill-current" />
Закреплено
</span>
) : null}
{headerActions}
</div>
</header>
{post.title ? (
<h3 className="app-text-wrap mb-1 text-base font-semibold text-[var(--foreground)]">
{post.title}
</h3>
) : null}
{postText ? (
<p className="app-text-wrap whitespace-pre-line text-sm leading-6 text-[var(--foreground)]">
{postText}
</p>
) : null}
{resolvedImageSrc ? (
<div className="mt-3 overflow-hidden rounded-xl">
<Image
src={resolvedImageSrc}
alt={post.title || "Публикация"}
width={1200}
height={720}
className="max-h-72 w-full object-cover"
unoptimized
/>
</div>
) : null}
<div className="app-text-muted mt-4 flex flex-wrap items-center justify-between gap-3 text-sm">
<div className="flex items-center gap-4 text-sm">
<div className="relative" {...likesWrapperProps}>
{onLikeToggle ? (
<button
type="button"
disabled={likeDisabled}
onClick={() => onLikeToggle(post)}
className={resolvedLikeButtonClassName}
>
<Heart
size={16}
className={
isLikeActive
? "fill-[var(--accent-primary)] text-[var(--accent-primary)]"
: "app-text-muted"
}
/>
{post.likes_count || 0}
</button>
) : (
<span className="inline-flex items-center gap-2">
<Heart size={16} />
{post.likes_count || 0}
</span>
)}
{likesPopover}
</div>
<button
type="button"
onClick={() => onOpenComments(post)}
className={commentsButtonClassName}
>
<MessageSquare size={16} className="app-text-muted" />
{post.comments_count || 0}
</button>
{attachmentHref ? (
<a
href={attachmentHref}
target="_blank"
rel="noopener noreferrer"
className={
attachmentButtonClassName ||
"app-action-ghost inline-flex items-center gap-2 rounded-lg px-3 py-2"
}
>
<Paperclip size={16} className="app-text-muted" />
Вложение
</a>
) : null}
</div>
{footerAction}
</div>
</article>
);
}