a tool for shared writing and social publishing
1"use client";
2import { AtUri } from "@atproto/api";
3import { PubIcon } from "components/ActionBar/Publications";
4import { CommentTiny } from "components/Icons/CommentTiny";
5import { QuoteTiny } from "components/Icons/QuoteTiny";
6import { Separator } from "components/Layout";
7import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider";
8import { BaseThemeProvider } from "components/ThemeManager/ThemeProvider";
9import { useSmoker } from "components/Toast";
10import { PubLeafletDocument, PubLeafletPublication } from "lexicons/api";
11import { blobRefToSrc } from "src/utils/blobRefToSrc";
12import type { Post } from "app/(home-pages)/reader/getReaderFeed";
13
14import Link from "next/link";
15import { InteractionPreview } from "./InteractionsPreview";
16import { useLocalizedDate } from "src/hooks/useLocalizedDate";
17
18export const PostListing = (props: Post) => {
19 let pubRecord = props.publication?.pubRecord as
20 | PubLeafletPublication.Record
21 | undefined;
22
23 let postRecord = props.documents.data as PubLeafletDocument.Record;
24 let postUri = new AtUri(props.documents.uri);
25 let uri = props.publication ? props.publication?.uri : props.documents.uri;
26
27 // For standalone documents (no publication), pass isStandalone to get correct defaults
28 let isStandalone = !pubRecord;
29 let theme = usePubTheme(pubRecord?.theme || postRecord?.theme, isStandalone);
30 let themeRecord = pubRecord?.theme || postRecord?.theme;
31 let backgroundImage =
32 themeRecord?.backgroundImage?.image?.ref && uri
33 ? blobRefToSrc(themeRecord.backgroundImage.image.ref, new AtUri(uri).host)
34 : null;
35
36 let backgroundImageRepeat = themeRecord?.backgroundImage?.repeat;
37 let backgroundImageSize = themeRecord?.backgroundImage?.width || 500;
38
39 let showPageBackground = pubRecord
40 ? pubRecord?.theme?.showPageBackground
41 : postRecord.theme?.showPageBackground ?? true;
42
43 let quotes = props.documents.document_mentions_in_bsky?.[0]?.count || 0;
44 let comments =
45 pubRecord?.preferences?.showComments === false
46 ? 0
47 : props.documents.comments_on_documents?.[0]?.count || 0;
48 let tags = (postRecord?.tags as string[] | undefined) || [];
49
50 // For standalone posts, link directly to the document
51 let postHref = props.publication
52 ? `${props.publication.href}/${postUri.rkey}`
53 : `/p/${postUri.host}/${postUri.rkey}`;
54
55 return (
56 <BaseThemeProvider {...theme} local>
57 <div
58 style={{
59 backgroundImage: backgroundImage
60 ? `url(${backgroundImage})`
61 : undefined,
62 backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat",
63 backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`,
64 }}
65 className={`no-underline! flex flex-row gap-2 w-full relative
66 bg-bg-leaflet
67 border border-border-light rounded-lg
68 sm:p-2 p-2 selected-outline
69 hover:outline-accent-contrast hover:border-accent-contrast
70 `}
71 >
72 <Link className="h-full w-full absolute top-0 left-0" href={postHref} />
73 <div
74 className={`${showPageBackground ? "bg-bg-page " : "bg-transparent"} rounded-md w-full px-[10px] pt-2 pb-2`}
75 style={{
76 backgroundColor: showPageBackground
77 ? "rgba(var(--bg-page), var(--bg-page-alpha))"
78 : "transparent",
79 }}
80 >
81 <h3 className="text-primary truncate">{postRecord.title}</h3>
82
83 <p className="text-secondary italic">{postRecord.description}</p>
84 <div className="flex flex-col-reverse md:flex-row md gap-2 text-sm text-tertiary items-center justify-start pt-1.5 md:pt-3 w-full">
85 {props.publication && pubRecord && (
86 <PubInfo
87 href={props.publication.href}
88 pubRecord={pubRecord}
89 uri={props.publication.uri}
90 />
91 )}
92 <div className="flex flex-row justify-between gap-2 items-center w-full">
93 <PostInfo publishedAt={postRecord.publishedAt} />
94 <InteractionPreview
95 postUrl={postHref}
96 quotesCount={quotes}
97 commentsCount={comments}
98 tags={tags}
99 showComments={pubRecord?.preferences?.showComments}
100 showMentions={pubRecord?.preferences?.showMentions}
101 share
102 />
103 </div>
104 </div>
105 </div>
106 </div>
107 </BaseThemeProvider>
108 );
109};
110
111const PubInfo = (props: {
112 href: string;
113 pubRecord: PubLeafletPublication.Record;
114 uri: string;
115}) => {
116 return (
117 <div className="flex flex-col md:w-auto shrink-0 w-full">
118 <hr className="md:hidden block border-border-light mb-2" />
119 <Link
120 href={props.href}
121 className="text-accent-contrast font-bold no-underline text-sm flex gap-1 items-center md:w-fit relative shrink-0"
122 >
123 <PubIcon small record={props.pubRecord} uri={props.uri} />
124 {props.pubRecord.name}
125 </Link>
126 </div>
127 );
128};
129
130const PostInfo = (props: { publishedAt: string | undefined }) => {
131 let localizedDate = useLocalizedDate(props.publishedAt || "", {
132 year: "numeric",
133 month: "short",
134 day: "numeric",
135 });
136 return (
137 <div className="flex gap-2 items-center shrink-0 self-start">
138 {props.publishedAt && (
139 <>
140 <div className="shrink-0">{localizedDate}</div>
141 </>
142 )}
143 </div>
144 );
145};