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">&middot;</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>
  );
}