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 share
101 />
102 </div>
103 </div>
104 </div>
105 </div>
106 </BaseThemeProvider>
107 );
108};
109
110const PubInfo = (props: {
111 href: string;
112 pubRecord: PubLeafletPublication.Record;
113 uri: string;
114}) => {
115 return (
116 <div className="flex flex-col md:w-auto shrink-0 w-full">
117 <hr className="md:hidden block border-border-light mb-2" />
118 <Link
119 href={props.href}
120 className="text-accent-contrast font-bold no-underline text-sm flex gap-1 items-center md:w-fit relative shrink-0"
121 >
122 <PubIcon small record={props.pubRecord} uri={props.uri} />
123 {props.pubRecord.name}
124 </Link>
125 </div>
126 );
127};
128
129const PostInfo = (props: { publishedAt: string | undefined }) => {
130 let localizedDate = useLocalizedDate(props.publishedAt || "", {
131 year: "numeric",
132 month: "short",
133 day: "numeric",
134 });
135 return (
136 <div className="flex gap-2 items-center shrink-0 self-start">
137 {props.publishedAt && (
138 <>
139 <div className="shrink-0">{localizedDate}</div>
140 </>
141 )}
142 </div>
143 );
144};