File size: 3,799 Bytes
c6302b1 492326f c6302b1 492326f 879c715 492326f c6302b1 879c715 c6302b1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 | import type { PooledTweet } from "@xtap-pool/shared";
import {
avatarColor,
displayName,
formatCount,
formatTweetDate,
isArticleTweet,
isRetweet,
photoMedia,
quotedTweetUrl,
tweetMetrics,
tweetTextHtml,
} from "../lib/format.js";
export type TweetCardProps = {
tweet: PooledTweet;
contributors: readonly string[];
now: Date;
};
/**
* Tweet text rendered to escaped HTML (URLs/mentions/hashtags linkified).
*/
function TweetText({ text }: { text: string }): React.JSX.Element {
return (
<div className="x-tweet-card__text" dangerouslySetInnerHTML={{ __html: tweetTextHtml(text) }} />
);
}
function TweetMedia({ tweet }: { tweet: PooledTweet }): React.JSX.Element | null {
const photos = photoMedia(tweet);
if (photos.length === 0) return null;
return (
<div className={`x-tweet-card__media x-tweet-card__media--count-${String(photos.length)}`}>
{photos.map((photo) => (
<a
key={photo.url}
className="x-tweet-card__media-link"
href={photo.url}
target="_blank"
rel="noopener noreferrer"
>
<img
className="x-tweet-card__media-image"
src={photo.url}
alt={photo.alt}
loading="lazy"
/>
</a>
))}
</div>
);
}
function Metrics({ tweet }: { tweet: PooledTweet }): React.JSX.Element {
const metrics = tweetMetrics(tweet);
const entries: [string, number][] = [
["replies", metrics.replies],
["reposts", metrics.retweets],
["likes", metrics.likes],
["views", metrics.views],
];
return (
<div className="x-tweet-card__metrics">
{entries.map(([label, value]) => (
<span key={label} className="x-tweet-card__metric" title={label}>
<span>{formatCount(value)}</span> {label}
</span>
))}
</div>
);
}
/** One tweet, rendered in the solmaz.io X-card style. */
export function TweetCard({ tweet, contributors, now }: TweetCardProps): React.JSX.Element {
const name = displayName(tweet);
const quoted = quotedTweetUrl(tweet);
return (
<article className="x-tweet-card">
<div
className="x-tweet-card__avatar"
style={{ background: avatarColor(tweet.author.username) }}
>
{name.slice(0, 1).toUpperCase()}
</div>
<div className="x-tweet-card__main">
<div className="x-tweet-card__identity">
<span className="x-tweet-card__name">{name}</span>
<span className="x-tweet-card__handle">@{tweet.author.username}</span>
<span className="x-tweet-card__dot">·</span>
<time dateTime={tweet.created_at ?? tweet.captured_at}>
{formatTweetDate(tweet.created_at ?? tweet.captured_at, now)}
</time>
<a
className="x-tweet-card__view-link"
href={tweet.url}
target="_blank"
rel="noopener noreferrer"
>
View on X
</a>
</div>
{isRetweet(tweet) ? <div className="x-tweet-card__handle">reposted</div> : null}
<TweetText text={tweet.text} />
<TweetMedia tweet={tweet} />
{quoted === undefined ? null : (
<a className="x-quote-card" href={quoted} target="_blank" rel="noopener noreferrer">
View quoted post on X
</a>
)}
<Metrics tweet={tweet} />
<div className="x-tweet-card__contributors">
{isArticleTweet(tweet) ? <span className="x-chip x-chip--article">article</span> : null}
{contributors.map((contributor) => (
<span key={contributor} className="x-chip" title={`captured by ${contributor}`}>
⛏ {contributor}
</span>
))}
</div>
</div>
</article>
);
}
|