Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
28 lines
3.1 KiB
TypeScript
28 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { Avatar } from "../ui/primitives";
|
|
import { ageLabel } from "../../lib/fixtures";
|
|
import type { StreamItem as FeedItem } from "../../lib/streams-data";
|
|
import type { Platform } from "../../lib/platforms";
|
|
import { SwipeItem } from "./swipe-item";
|
|
|
|
export function PostCard({ item, platform, onAmplify, onReply, match = "", selected = false, onSelect }: { item: FeedItem; platform: Platform; onAmplify: () => void; onReply: () => void; match?: string; selected?: boolean; onSelect?: () => void }): React.ReactElement {
|
|
const index = match.trim() === "" ? -1 : item.text.toLowerCase().indexOf(match.trim().toLowerCase());
|
|
const text = index < 0 ? item.text : <>{item.text.slice(0, index)}<strong className="font-bold text-accent">{item.text.slice(index, index + match.trim().length)}</strong>{item.text.slice(index + match.trim().length)}</>;
|
|
return <SwipeItem leftActions={[{ label: platform.amplifyLabels.repost ?? platform.amplifyLabels.share ?? "Amplify", onClick: onAmplify, tone: "accent" }, { label: "Quote", onClick: onAmplify }, { label: "Crosspost", onClick: onAmplify }]}>
|
|
<article onMouseEnter={onSelect} data-stream-selected={selected || undefined} className={`border-b border-border-subtle px-4 py-4 ${selected ? "bg-accent-dim" : ""}`} data-post-card>
|
|
<div className="flex gap-3"><Avatar handle={item.handle} /><div className="min-w-0 flex-1">
|
|
<div className="flex min-w-0 items-center gap-2"><span className="truncate text-sm font-semibold">{item.author}</span><span className="truncate text-xs text-text-faint">{item.handle}</span><span className="ml-auto shrink-0 text-xs text-text-faint">{ageLabel(item.ageMinutes)}</span></div>
|
|
<span className="mt-1 inline-flex rounded-full border border-border px-2 py-0.5 text-2xs text-text-dim"><span className="mr-1.5 h-2 w-2 self-center rounded-full bg-accent" />{item.sourceLabel ?? platform.displayName}</span>
|
|
<p className="mt-3 whitespace-pre-wrap break-words text-sm leading-relaxed">{text}</p>
|
|
{item.mediaUrls?.map((url) => <a key={url} href={url} target="_blank" rel="noreferrer" className="mr-2 text-xs text-accent">View media ↗</a>)}
|
|
{item.hasMedia && !item.mediaUrls && <div className="mt-3 flex min-h-28 items-center justify-center rounded-lg border border-border-subtle bg-surface-2 text-xs text-text-faint">Media preview · sample feed</div>}
|
|
<div className="mt-3 flex items-center justify-between text-xs text-text-faint">
|
|
<button type="button" className="min-h-11 min-w-11 text-left" onClick={onReply} aria-label={`Reply to ${item.handle}`}>↩ {item.replies}</button>
|
|
<button type="button" className={`min-h-11 min-w-11 ${item.connectionId === undefined ? "opacity-50" : ""}`} onClick={onAmplify} title={item.connectionId === undefined ? "not connected — connect an account to boost" : undefined} aria-label={`Amplify ${item.handle}`}>↻ {item.reposts}</button>
|
|
<span className="flex min-h-11 min-w-11 items-center justify-end" aria-label={`${item.likes} likes`}>♡ {item.likes}</span>
|
|
</div>
|
|
</div></div>
|
|
</article>
|
|
</SwipeItem>;
|
|
}
|