Write on the margins of the internet. Powered by the AT Protocol.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #13 from icorbrey-contrib/icorbrey/user-meta

users: Extract user meta from cards

authored by

Scan and committed by
GitHub
300af0f8 30cc9e80

+68 -165
+3 -109
web/src/components/AnnotationCard.jsx
··· 27 27 } from "lucide-react"; 28 28 import { HighlightIcon, TrashIcon } from "./Icons"; 29 29 import ShareMenu from "./ShareMenu"; 30 + import UserMeta from "./UserMeta"; 30 31 31 32 function buildTextFragmentUrl(baseUrl, selector) { 32 33 if (!selector || selector.type !== "TextQuoteSelector" || !selector.exact) { ··· 173 174 } 174 175 }; 175 176 176 - const formatDate = (dateString, simple = true) => { 177 - if (!dateString) return ""; 178 - const date = new Date(dateString); 179 - const now = new Date(); 180 - const diff = now - date; 181 - const minutes = Math.floor(diff / 60000); 182 - const hours = Math.floor(diff / 3600000); 183 - const days = Math.floor(diff / 86400000); 184 - if (minutes < 1) return "just now"; 185 - if (minutes < 60) return `${minutes}m`; 186 - if (hours < 24) return `${hours}h`; 187 - if (days < 7) return `${days}d`; 188 - if (simple) 189 - return date.toLocaleDateString("en-US", { 190 - month: "short", 191 - day: "numeric", 192 - }); 193 - return date.toLocaleString(); 194 - }; 195 - 196 - const authorDisplayName = data.author?.displayName || data.author?.handle; 197 - const authorHandle = data.author?.handle; 198 - const authorAvatar = data.author?.avatar; 199 - const authorDid = data.author?.did; 200 - const marginProfileUrl = authorDid ? `/profile/${authorDid}` : null; 201 177 const highlightedText = 202 178 data.selector?.type === "TextQuoteSelector" ? data.selector.exact : null; 203 179 const fragmentUrl = buildTextFragmentUrl(data.url, data.selector); ··· 245 221 <article className="card annotation-card"> 246 222 <header className="annotation-header"> 247 223 <div className="annotation-header-left"> 248 - <Link to={marginProfileUrl || "#"} className="annotation-avatar-link"> 249 - <div className="annotation-avatar"> 250 - {authorAvatar ? ( 251 - <img src={authorAvatar} alt={authorDisplayName} /> 252 - ) : ( 253 - <span> 254 - {(authorDisplayName || authorHandle || "??") 255 - ?.substring(0, 2) 256 - .toUpperCase()} 257 - </span> 258 - )} 259 - </div> 260 - </Link> 261 - <div className="annotation-meta"> 262 - <div className="annotation-author-row"> 263 - <Link 264 - to={marginProfileUrl || "#"} 265 - className="annotation-author-link" 266 - > 267 - <span className="annotation-author">{authorDisplayName}</span> 268 - </Link> 269 - {authorHandle && ( 270 - <a 271 - href={`https://bsky.app/profile/${authorHandle}`} 272 - target="_blank" 273 - rel="noopener noreferrer" 274 - className="annotation-handle" 275 - > 276 - @{authorHandle} 277 - </a> 278 - )} 279 - </div> 280 - <div className="annotation-time">{formatDate(data.createdAt)}</div> 281 - </div> 224 + <UserMeta author={data.author} createdAt={data.createdAt} /> 282 225 </div> 283 226 <div className="annotation-header-right"> 284 227 <div style={{ display: "flex", gap: "4px" }}> ··· 606 549 } 607 550 }; 608 551 609 - const formatDate = (dateString, simple = true) => { 610 - if (!dateString) return ""; 611 - const date = new Date(dateString); 612 - const now = new Date(); 613 - const diff = now - date; 614 - const minutes = Math.floor(diff / 60000); 615 - const hours = Math.floor(diff / 3600000); 616 - const days = Math.floor(diff / 86400000); 617 - if (minutes < 1) return "just now"; 618 - if (minutes < 60) return `${minutes}m`; 619 - if (hours < 24) return `${hours}h`; 620 - if (days < 7) return `${days}d`; 621 - if (simple) 622 - return date.toLocaleDateString("en-US", { 623 - month: "short", 624 - day: "numeric", 625 - }); 626 - return date.toLocaleString(); 627 - }; 628 - 629 552 return ( 630 553 <article className="card annotation-card"> 631 554 <header className="annotation-header"> 632 555 <div className="annotation-header-left"> 633 - <Link 634 - to={data.author?.did ? `/profile/${data.author.did}` : "#"} 635 - className="annotation-avatar-link" 636 - > 637 - <div className="annotation-avatar"> 638 - {data.author?.avatar ? ( 639 - <img src={data.author.avatar} alt="avatar" /> 640 - ) : ( 641 - <span>??</span> 642 - )} 643 - </div> 644 - </Link> 645 - <div className="annotation-meta"> 646 - <Link to="#" className="annotation-author-link"> 647 - <span className="annotation-author"> 648 - {data.author?.displayName || "Unknown"} 649 - </span> 650 - </Link> 651 - <div className="annotation-time">{formatDate(data.createdAt)}</div> 652 - {data.author?.handle && ( 653 - <a 654 - href={`https://bsky.app/profile/${data.author.handle}`} 655 - target="_blank" 656 - rel="noopener noreferrer" 657 - className="annotation-handle" 658 - > 659 - @{data.author.handle} 660 - </a> 661 - )} 662 - </div> 556 + <UserMeta author={data.author} createdAt={data.createdAt} /> 663 557 </div> 664 558 665 559 <div className="annotation-header-right">
+2 -56
web/src/components/BookmarkCard.jsx
··· 1 1 import { useState, useEffect } from "react"; 2 2 import { useAuth } from "../context/AuthContext"; 3 - import { Link } from "react-router-dom"; 4 3 import { 5 4 normalizeAnnotation, 6 5 normalizeBookmark, ··· 12 11 import { HeartIcon, TrashIcon, BookmarkIcon } from "./Icons"; 13 12 import { Folder } from "lucide-react"; 14 13 import ShareMenu from "./ShareMenu"; 14 + import UserMeta from "./UserMeta"; 15 15 16 16 export default function BookmarkCard({ 17 17 bookmark, ··· 90 90 } 91 91 }; 92 92 93 - const formatDate = (dateString) => { 94 - if (!dateString) return ""; 95 - const date = new Date(dateString); 96 - const now = new Date(); 97 - const diff = now - date; 98 - const minutes = Math.floor(diff / 60000); 99 - const hours = Math.floor(diff / 3600000); 100 - const days = Math.floor(diff / 86400000); 101 - if (minutes < 1) return "just now"; 102 - if (minutes < 60) return `${minutes}m`; 103 - if (hours < 24) return `${hours}h`; 104 - if (days < 7) return `${days}d`; 105 - return date.toLocaleDateString("en-US", { month: "short", day: "numeric" }); 106 - }; 107 - 108 93 let domain = ""; 109 94 try { 110 95 if (data.url) domain = new URL(data.url).hostname.replace("www.", ""); 111 96 } catch { 112 97 /* ignore */ 113 98 } 114 - 115 - const authorDisplayName = data.author?.displayName || data.author?.handle; 116 - const authorHandle = data.author?.handle; 117 - const authorAvatar = data.author?.avatar; 118 - const authorDid = data.author?.did; 119 - const marginProfileUrl = authorDid ? `/profile/${authorDid}` : null; 120 99 121 100 return ( 122 101 <article className="card annotation-card bookmark-card"> 123 102 <header className="annotation-header"> 124 103 <div className="annotation-header-left"> 125 - <Link to={marginProfileUrl || "#"} className="annotation-avatar-link"> 126 - <div className="annotation-avatar"> 127 - {authorAvatar ? ( 128 - <img src={authorAvatar} alt={authorDisplayName} /> 129 - ) : ( 130 - <span> 131 - {(authorDisplayName || authorHandle || "??") 132 - ?.substring(0, 2) 133 - .toUpperCase()} 134 - </span> 135 - )} 136 - </div> 137 - </Link> 138 - <div className="annotation-meta"> 139 - <div className="annotation-author-row"> 140 - <Link 141 - to={marginProfileUrl || "#"} 142 - className="annotation-author-link" 143 - > 144 - <span className="annotation-author">{authorDisplayName}</span> 145 - </Link> 146 - {authorHandle && ( 147 - <a 148 - href={`https://bsky.app/profile/${authorHandle}`} 149 - target="_blank" 150 - rel="noopener noreferrer" 151 - className="annotation-handle" 152 - > 153 - @{authorHandle} 154 - </a> 155 - )} 156 - </div> 157 - <div className="annotation-time">{formatDate(data.createdAt)}</div> 158 - </div> 104 + <UserMeta author={data.author} createdAt={data.createdAt} /> 159 105 </div> 160 106 161 107 <div className="annotation-header-right">
+63
web/src/components/UserMeta.jsx
··· 1 + import { Link } from "react-router-dom"; 2 + 3 + const formatDate = (dateString, simple = true) => { 4 + if (!dateString) return ""; 5 + const date = new Date(dateString); 6 + const now = new Date(); 7 + const diff = now - date; 8 + const minutes = Math.floor(diff / 60000); 9 + const hours = Math.floor(diff / 3600000); 10 + const days = Math.floor(diff / 86400000); 11 + if (minutes < 1) return "just now"; 12 + if (minutes < 60) return `${minutes}m`; 13 + if (hours < 24) return `${hours}h`; 14 + if (days < 7) return `${days}d`; 15 + if (simple) 16 + return date.toLocaleDateString("en-US", { 17 + month: "short", 18 + day: "numeric", 19 + }); 20 + return date.toLocaleString(); 21 + }; 22 + 23 + export default function UserMeta({ author, createdAt }) { 24 + const authorDisplayName = author?.displayName || author?.handle || "Unknown"; 25 + const authorHandle = author?.handle; 26 + const authorAvatar = author?.avatar; 27 + const authorDid = author?.did; 28 + const marginProfileUrl = authorDid ? `/profile/${authorDid}` : "#"; 29 + 30 + return ( 31 + <> 32 + <Link to={marginProfileUrl} className="annotation-avatar-link"> 33 + <div className="annotation-avatar"> 34 + {authorAvatar ? ( 35 + <img src={authorAvatar} alt={authorDisplayName} /> 36 + ) : ( 37 + <span> 38 + {authorDisplayName?.substring(0, 2).toUpperCase() || "??"} 39 + </span> 40 + )} 41 + </div> 42 + </Link> 43 + <div className="annotation-meta"> 44 + <div className="annotation-author-row"> 45 + <Link to={marginProfileUrl} className="annotation-author-link"> 46 + <span className="annotation-author">{authorDisplayName}</span> 47 + </Link> 48 + {authorHandle && ( 49 + <a 50 + href={`https://bsky.app/profile/${authorHandle}`} 51 + target="_blank" 52 + rel="noopener noreferrer" 53 + className="annotation-handle" 54 + > 55 + @{authorHandle} 56 + </a> 57 + )} 58 + </div> 59 + <div className="annotation-time">{formatDate(createdAt)}</div> 60 + </div> 61 + </> 62 + ); 63 + }