Leaflet Blog in Deno Fresh

more

-284
components/bsky-comments/Comment.tsx
···
-
import { AppBskyFeedDefs, AppBskyFeedPost } from "npm:@atproto/api";
-
-
type CommentProps = {
-
comment: AppBskyFeedDefs.ThreadViewPost;
-
filters?: Array<(arg: any) => boolean>;
-
};
-
-
export const Comment = ({ comment, filters }: CommentProps) => {
-
const author = comment.post.author;
-
const avatarClassName = "avatar";
-
-
if (!AppBskyFeedPost.isRecord(comment.post.record)) return null;
-
// filter out replies that match any of the commentFilters, by ensuring they all return false
-
if (filters && !filters.every((filter) => !filter(comment))) return null;
-
-
const styles = `
-
.container {
-
max-width: 740px;
-
margin: 0 auto;
-
}
-
-
.statsBar {
-
display: flex;
-
align-items: center;
-
gap: 0.5rem;
-
}
-
-
.statsBar:hover {
-
text-decoration: underline;
-
}
-
-
.statItem {
-
display: flex;
-
align-items: center;
-
gap: 0.25rem;
-
white-space: nowrap;
-
}
-
-
.container a.link {
-
text-decoration: underline;
-
}
-
-
.container a.link:hover {
-
text-decoration: underline;
-
}
-
-
.icon {
-
width: 1.25rem;
-
height: 1.25rem;
-
}
-
-
.errorText, .loadingText {
-
text-align: center;
-
}
-
-
.commentsTitle {
-
margin-top: 1.5rem;
-
font-size: 1.25rem;
-
font-weight: bold;
-
}
-
-
.replyText {
-
margin-top: 0.5rem;
-
font-size: 0.875rem;
-
}
-
-
.divider {
-
margin-top: 0.5rem;
-
}
-
-
.commentsList {
-
margin-top: 0.5rem;
-
display: flex;
-
flex-direction: column;
-
gap: 0.5rem;
-
}
-
-
.container .showMoreButton {
-
margin-top: 0.5rem;
-
font-size: 0.875rem;
-
text-decoration: underline;
-
}
-
-
.container .showMoreButton:hover {
-
text-decoration: underline;
-
}
-
-
.commentContainer {
-
margin: 1rem 0;
-
font-size: 0.875rem;
-
}
-
-
.commentContent {
-
display: flex;
-
max-width: 36rem;
-
flex-direction: column;
-
gap: 0.5rem;
-
}
-
-
.authorLink {
-
display: flex;
-
flex-direction: row;
-
justify-content: flex-start;
-
align-items: center;
-
gap: 0.5rem;
-
}
-
-
.authorLink:hover {
-
text-decoration: underline;
-
}
-
-
.avatar {
-
height: 1rem;
-
width: 1rem;
-
flex-shrink: 0;
-
border-radius: 9999px;
-
background-color: #d1d5db;
-
}
-
-
.authorName {
-
overflow: hidden;
-
text-overflow: ellipsis;
-
white-space: nowrap;
-
display: -webkit-box;
-
-webkit-line-clamp: 1;
-
-webkit-box-orient: vertical;
-
}
-
-
.container a {
-
text-decoration: none;
-
color: inherit;
-
}
-
-
.container a:hover {
-
text-decoration: none;
-
}
-
-
.commentContent .handle {
-
color: #6b7280;
-
}
-
.repliesContainer {
-
border-left: 2px solid #525252;
-
padding-left: 0.5rem;
-
}
-
-
.actionsContainer {
-
margin-top: 0.5rem;
-
display: flex;
-
width: 100%;
-
max-width: 150px;
-
flex-direction: row;
-
align-items: center;
-
justify-content: space-between;
-
opacity: 0.6;
-
}
-
-
-
.actionsRow {
-
display: flex;
-
align-items: center;
-
gap: 0.25rem;
-
}
-
`;
-
-
return (
-
<div className="commentContainer">
-
<style>{styles}</style>
-
<div className="commentContent">
-
<a
-
className="authorLink"
-
href={`https://bsky.app/profile/${author.did}`}
-
target="_blank"
-
rel="noreferrer noopener"
-
>
-
{author.avatar
-
? (
-
<img
-
src={comment.post.author.avatar}
-
alt="avatar"
-
className={avatarClassName}
-
/>
-
)
-
: <div className={avatarClassName} />}
-
<p className="authorName">
-
{author.displayName ?? author.handle}{" "}
-
<span className="handle">@{author.handle}</span>
-
</p>
-
</a>
-
<a
-
href={`https://bsky.app/profile/${author.did}/post/${
-
comment.post.uri
-
.split("/")
-
.pop()
-
}`}
-
target="_blank"
-
rel="noreferrer noopener"
-
>
-
<p>{(comment.post.record as AppBskyFeedPost.Record).text}</p>
-
<Actions post={comment.post} />
-
</a>
-
</div>
-
{comment.replies && comment.replies.length > 0 && (
-
<div className="repliesContainer">
-
{comment.replies.sort(sortByLikes).map((reply) => {
-
if (!AppBskyFeedDefs.isThreadViewPost(reply)) return null;
-
return (
-
<Comment key={reply.post.uri} comment={reply} filters={filters} />
-
);
-
})}
-
</div>
-
)}
-
</div>
-
);
-
};
-
-
const Actions = ({ post }: { post: AppBskyFeedDefs.PostView }) => (
-
<div className="actionsContainer">
-
<div className="actionsRow">
-
<svg
-
className="icon"
-
xmlns="http://www.w3.org/2000/svg"
-
fill="none"
-
viewBox="0 0 24 24"
-
strokeWidth="1.5"
-
stroke="currentColor"
-
>
-
<path
-
strokeLinecap="round"
-
strokeLinejoin="round"
-
d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 0 1-.923 1.785A5.969 5.969 0 0 0 6 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337Z"
-
/>
-
</svg>
-
<p className="text-xs">{post.replyCount ?? 0}</p>
-
</div>
-
<div className="actionsRow">
-
<svg
-
className="icon"
-
xmlns="http://www.w3.org/2000/svg"
-
fill="none"
-
viewBox="0 0 24 24"
-
strokeWidth="1.5"
-
stroke="currentColor"
-
>
-
<path
-
strokeLinecap="round"
-
strokeLinejoin="round"
-
d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 0 0-3.7-3.7 48.678 48.678 0 0 0-7.324 0 4.006 4.006 0 0 0-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 0 0 3.7 3.7 48.656 48.656 0 0 0 7.324 0 4.006 4.006 0 0 0 3.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3-3 3"
-
/>
-
</svg>
-
<p className="text-xs">{post.repostCount ?? 0}</p>
-
</div>
-
<div className="actionsRow">
-
<svg
-
className="icon"
-
xmlns="http://www.w3.org/2000/svg"
-
fill="none"
-
viewBox="0 0 24 24"
-
strokeWidth="1.5"
-
stroke="currentColor"
-
>
-
<path
-
strokeLinecap="round"
-
strokeLinejoin="round"
-
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
-
/>
-
</svg>
-
<p className="text-xs">{post.likeCount ?? 0}</p>
-
</div>
-
</div>
-
);
-
-
const sortByLikes = (a: unknown, b: unknown) => {
-
if (
-
!AppBskyFeedDefs.isThreadViewPost(a) ||
-
!AppBskyFeedDefs.isThreadViewPost(b) ||
-
!("post" in a) ||
-
!("post" in b)
-
) {
-
return 0;
-
}
-
const aPost = a as AppBskyFeedDefs.ThreadViewPost;
-
const bPost = b as AppBskyFeedDefs.ThreadViewPost;
-
return (bPost.post.likeCount ?? 0) - (aPost.post.likeCount ?? 0);
-
};
-75
components/bsky-comments/CommentFilters.tsx
···
-
import { type AppBskyFeedDefs, AppBskyFeedPost } from "npm:@atproto/api";
-
-
const MinLikeCountFilter = (
-
min: number,
-
): (comment: AppBskyFeedDefs.ThreadViewPost) => boolean => {
-
return (comment: AppBskyFeedDefs.ThreadViewPost) => {
-
return (comment.post.likeCount ?? 0) < min;
-
};
-
};
-
-
const MinCharacterCountFilter = (
-
min: number,
-
): (comment: AppBskyFeedDefs.ThreadViewPost) => boolean => {
-
return (comment: AppBskyFeedDefs.ThreadViewPost) => {
-
if (!AppBskyFeedPost.isRecord(comment.post.record)) {
-
return false;
-
}
-
const record = comment.post.record as AppBskyFeedPost.Record;
-
return record.text.length < min;
-
};
-
};
-
-
const TextContainsFilter = (
-
text: string,
-
): (comment: AppBskyFeedDefs.ThreadViewPost) => boolean => {
-
return (comment: AppBskyFeedDefs.ThreadViewPost) => {
-
if (!AppBskyFeedPost.isRecord(comment.post.record)) {
-
return false;
-
}
-
const record = comment.post.record as AppBskyFeedPost.Record;
-
return record.text.toLowerCase().includes(text.toLowerCase());
-
};
-
};
-
-
const ExactMatchFilter = (
-
text: string,
-
): (comment: AppBskyFeedDefs.ThreadViewPost) => boolean => {
-
return (comment: AppBskyFeedDefs.ThreadViewPost) => {
-
if (!AppBskyFeedPost.isRecord(comment.post.record)) {
-
return false;
-
}
-
const record = comment.post.record as AppBskyFeedPost.Record;
-
return record.text.toLowerCase() === text.toLowerCase();
-
};
-
};
-
-
/*
-
* This function allows you to filter out comments based on likes,
-
* characters, text, pins, or exact matches.
-
*/
-
export const Filters: {
-
MinLikeCountFilter: (
-
min: number,
-
) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
-
MinCharacterCountFilter: (
-
min: number,
-
) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
-
TextContainsFilter: (
-
text: string,
-
) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
-
ExactMatchFilter: (
-
text: string,
-
) => (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
-
NoLikes: (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
-
NoPins: (comment: AppBskyFeedDefs.ThreadViewPost) => boolean;
-
} = {
-
MinLikeCountFilter,
-
MinCharacterCountFilter,
-
TextContainsFilter,
-
ExactMatchFilter,
-
NoLikes: MinLikeCountFilter(0),
-
NoPins: ExactMatchFilter("📌"),
-
};
-
-
export default Filters;
-232
components/bsky-comments/PostSummary.tsx
···
-
import { AppBskyFeedDefs } from "npm:@atproto/api";
-
-
type PostSummaryProps = {
-
postUrl: string;
-
post: AppBskyFeedDefs.PostView;
-
};
-
-
export const PostSummary = ({ postUrl, post }: PostSummaryProps) => {
-
const styles = `
-
.container {
-
max-width: 740px;
-
margin: 0 auto;
-
}
-
-
.statsBar {
-
display: flex;
-
align-items: center;
-
gap: 0.5rem;
-
}
-
-
.statsBar:hover {
-
text-decoration: underline;
-
}
-
-
.statItem {
-
display: flex;
-
align-items: center;
-
gap: 0.25rem;
-
white-space: nowrap;
-
}
-
-
.container a.link {
-
text-decoration: underline;
-
}
-
-
.container a.link:hover {
-
text-decoration: underline;
-
}
-
-
.icon {
-
width: 1.25rem;
-
height: 1.25rem;
-
}
-
-
.errorText, .loadingText {
-
text-align: center;
-
}
-
-
.commentsTitle {
-
margin-top: 1.5rem;
-
font-size: 1.25rem;
-
font-weight: bold;
-
}
-
-
.replyText {
-
margin-top: 0.5rem;
-
font-size: 0.875rem;
-
}
-
-
.divider {
-
margin-top: 0.5rem;
-
}
-
-
.commentsList {
-
margin-top: 0.5rem;
-
display: flex;
-
flex-direction: column;
-
gap: 0.5rem;
-
}
-
-
.container .showMoreButton {
-
margin-top: 0.5rem;
-
font-size: 0.875rem;
-
text-decoration: underline;
-
}
-
-
.container .showMoreButton:hover {
-
text-decoration: underline;
-
}
-
-
.commentContainer {
-
margin: 1rem 0;
-
font-size: 0.875rem;
-
}
-
-
.commentContent {
-
display: flex;
-
max-width: 36rem;
-
flex-direction: column;
-
gap: 0.5rem;
-
}
-
-
.authorLink {
-
display: flex;
-
flex-direction: row;
-
justify-content: flex-start;
-
align-items: center;
-
gap: 0.5rem;
-
}
-
-
.authorLink:hover {
-
text-decoration: underline;
-
}
-
-
.avatar {
-
height: 1rem;
-
width: 1rem;
-
flex-shrink: 0;
-
border-radius: 9999px;
-
background-color: #d1d5db;
-
}
-
-
.authorName {
-
overflow: hidden;
-
text-overflow: ellipsis;
-
white-space: nowrap;
-
display: -webkit-box;
-
-webkit-line-clamp: 1;
-
-webkit-box-orient: vertical;
-
}
-
-
.container a {
-
text-decoration: none;
-
color: inherit;
-
}
-
-
.container a:hover {
-
text-decoration: none;
-
}
-
-
.commentContent .handle {
-
color: #6b7280;
-
}
-
.repliesContainer {
-
border-left: 2px solid #525252;
-
padding-left: 0.5rem;
-
}
-
-
.actionsContainer {
-
margin-top: 0.5rem;
-
display: flex;
-
width: 100%;
-
max-width: 150px;
-
flex-direction: row;
-
align-items: center;
-
justify-content: space-between;
-
opacity: 0.6;
-
}
-
-
-
.actionsRow {
-
display: flex;
-
align-items: center;
-
gap: 0.25rem;
-
}
-
`;
-
-
return (
-
<>
-
<style>{styles}</style>
-
<a href={postUrl} target="_blank" rel="noreferrer noopener">
-
<p className="statsBar">
-
<span className="statItem">
-
<svg
-
className="icon"
-
xmlns="http://www.w3.org/2000/svg"
-
fill="pink"
-
viewBox="0 0 24 24"
-
strokeWidth="1.5"
-
stroke="pink"
-
flood-color="pink"
-
>
-
<path
-
strokeLinecap="round"
-
strokeLinejoin="round"
-
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
-
/>
-
</svg>
-
<span>{post.likeCount ?? 0} likes</span>
-
</span>
-
<span className="statItem">
-
<svg
-
className="icon"
-
xmlns="http://www.w3.org/2000/svg"
-
fill="none"
-
viewBox="0 0 24 24"
-
strokeWidth="1.5"
-
stroke="green"
-
>
-
<path
-
strokeLinecap="round"
-
strokeLinejoin="round"
-
d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 0 0-3.7-3.7 48.678 48.678 0 0 0-7.324 0 4.006 4.006 0 0 0-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 0 0 3.7 3.7 48.656 48.656 0 0 0 7.324 0 4.006 4.006 0 0 0 3.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3-3 3"
-
/>
-
</svg>
-
<span>{post.repostCount ?? 0} reposts</span>
-
</span>
-
<span className="statItem">
-
<svg
-
className="icon"
-
xmlns="http://www.w3.org/2000/svg"
-
fill="#7FBADC"
-
viewBox="0 0 24 24"
-
strokeWidth="1.5"
-
stroke="#7FBADC"
-
>
-
<path
-
strokeLinecap="round"
-
strokeLinejoin="round"
-
d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 0 1-.923 1.785A5.969 5.969 0 0 0 6 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337Z"
-
/>
-
</svg>
-
<span>{post.replyCount ?? 0} replies</span>
-
</span>
-
</p>
-
</a>
-
<h2 className="commentsTitle">Comments</h2>
-
<p className="replyText">
-
Join the conversation by{" "}
-
<a
-
className="link"
-
href={postUrl}
-
target="_blank"
-
rel="noreferrer noopener"
-
>
-
replying on Bluesky
-
</a>
-
.
-
</p>
-
</>
-
);
-
};
-11
components/bsky-comments/types.tsx
···
-
export interface CommentEmptyDetails {
-
code: string;
-
message: string;
-
}
-
-
export interface CommentOptions {
-
uri?: string;
-
author?: string;
-
commentFilters?: Array<(arg: any) => boolean>;
-
onEmpty?: (details: CommentEmptyDetails) => void;
-
}
+1 -1
components/typography.tsx
···
return (
<Tag
className={cx(
-
"font-serif font-bold text-balance tracking-wide scroll-m-20 uppercase mt-8 [&>code]:text-[length:inherit] first:mt-0",
+
"font-serif font-bold tracking-wide scroll-m-20 uppercase mt-8 [&>code]:text-[length:inherit] first:mt-0 break-words text-wrap",
style,
className?.toString(),
)}
-2
fresh.gen.ts
···
import * as $index from "./routes/index.tsx";
import * as $post_slug_ from "./routes/post/[slug].tsx";
import * as $rss from "./routes/rss.ts";
-
import * as $CommentSection from "./islands/CommentSection.tsx";
import * as $layout from "./islands/layout.tsx";
import * as $post_list from "./islands/post-list.tsx";
import type { Manifest } from "$fresh/server.ts";
···
"./routes/rss.ts": $rss,
},
islands: {
-
"./islands/CommentSection.tsx": $CommentSection,
"./islands/layout.tsx": $layout,
"./islands/post-list.tsx": $post_list,
},
-431
islands/CommentSection.tsx
···
-
import { useEffect, useState } from "preact/hooks";
-
import {
-
AppBskyFeedDefs,
-
type AppBskyFeedGetPostThread,
-
} from "npm:@atproto/api";
-
import { CommentOptions } from "../components/bsky-comments/types.tsx";
-
import { PostSummary } from "../components/bsky-comments/PostSummary.tsx";
-
import { Comment } from "../components/bsky-comments/Comment.tsx";
-
-
const getAtUri = (uri: string): string => {
-
if (!uri.startsWith("at://") && uri.includes("bsky.app/profile/")) {
-
const match = uri.match(/profile\/([\w:.]+)\/post\/([\w]+)/);
-
if (match) {
-
const [, did, postId] = match;
-
return `at://${did}/app.bsky.feed.post/${postId}`;
-
}
-
}
-
return uri;
-
};
-
-
/**
-
* This component displays a comment section for a post.
-
* It fetches the comments for a post and displays them in a threaded format.
-
*/
-
export const CommentSection = ({
-
uri: propUri,
-
author,
-
onEmpty,
-
commentFilters,
-
}: CommentOptions): any => {
-
const [uri, setUri] = useState<string | null>(null);
-
const [thread, setThread] = useState<AppBskyFeedDefs.ThreadViewPost | null>(
-
null,
-
);
-
const [error, setError] = useState<string | null>(null);
-
const [visibleCount, setVisibleCount] = useState(5);
-
-
const styles = `
-
.container {
-
max-width: 740px;
-
margin: 0 auto;
-
}
-
-
.statsBar {
-
display: flex;
-
align-items: center;
-
gap: 0.5rem;
-
}
-
-
.statsBar:hover {
-
text-decoration: underline;
-
}
-
-
.statItem {
-
display: flex;
-
align-items: center;
-
gap: 0.25rem;
-
white-space: nowrap;
-
}
-
-
.container a.link {
-
text-decoration: underline;
-
}
-
-
.container a.link:hover {
-
text-decoration: underline;
-
}
-
-
.icon {
-
width: 1.25rem;
-
height: 1.25rem;
-
}
-
-
.errorText, .loadingText {
-
text-align: center;
-
}
-
-
.commentsTitle {
-
margin-top: 1.5rem;
-
font-size: 1.25rem;
-
font-weight: bold;
-
}
-
-
.replyText {
-
margin-top: 0.5rem;
-
font-size: 0.875rem;
-
}
-
-
.divider {
-
margin-top: 0.5rem;
-
}
-
-
.commentsList {
-
margin-top: 0.5rem;
-
display: flex;
-
flex-direction: column;
-
gap: 0.5rem;
-
}
-
-
.container .showMoreButton {
-
margin-top: 0.5rem;
-
font-size: 0.875rem;
-
text-decoration: underline;
-
cursor: pointer;
-
}
-
-
.container .showMoreButton:hover {
-
text-decoration: underline;
-
}
-
-
.commentContainer {
-
margin: 1rem 0;
-
font-size: 0.875rem;
-
}
-
-
.commentContent {
-
display: flex;
-
max-width: 36rem;
-
flex-direction: column;
-
gap: 0.5rem;
-
}
-
-
.authorLink {
-
display: flex;
-
flex-direction: row;
-
justify-content: flex-start;
-
align-items: center;
-
gap: 0.5rem;
-
}
-
-
.authorLink:hover {
-
text-decoration: underline;
-
}
-
-
.avatar {
-
height: 1rem;
-
width: 1rem;
-
flex-shrink: 0;
-
border-radius: 9999px;
-
background-color: #d1d5db;
-
}
-
-
.authorName {
-
overflow: hidden;
-
text-overflow: ellipsis;
-
white-space: nowrap;
-
display: -webkit-box;
-
-webkit-line-clamp: 1;
-
-webkit-box-orient: vertical;
-
}
-
-
.container a {
-
text-decoration: none;
-
color: inherit;
-
}
-
-
.container a:hover {
-
text-decoration: none;
-
}
-
-
.commentContent .handle {
-
color: #6b7280;
-
}
-
.repliesContainer {
-
border-left: 2px solid #525252;
-
padding-left: 0.5rem;
-
}
-
-
.actionsContainer {
-
margin-top: 0.5rem;
-
display: flex;
-
width: 100%;
-
max-width: 150px;
-
flex-direction: row;
-
align-items: center;
-
justify-content: space-between;
-
opacity: 0.6;
-
}
-
-
-
.actionsRow {
-
display: flex;
-
align-items: center;
-
gap: 0.25rem;
-
}
-
-
-
.font-sans { font-family: var(--font-sans); }
-
.font-serif { font-family: var(--font-serif); }
-
.font-mono { font-family: var(--font-mono); }
-
-
h1 {
-
font-family: var(--font-serif);
-
text-transform: uppercase;
-
font-size: 2.25rem;
-
}
-
-
h2 {
-
font-family: var(--font-serif);
-
text-transform: uppercase;
-
font-size: 1.75rem;
-
}
-
-
h3 {
-
font-family: var(--font-serif);
-
text-transform: uppercase;
-
font-size: 1.5rem;
-
}
-
-
h4 {
-
font-family: var(--font-serif);
-
text-transform: uppercase;
-
font-size: 1.25rem;
-
}
-
-
h5 {
-
font-family: var(--font-serif);
-
text-transform: uppercase;
-
font-size: 1rem;
-
}
-
-
h6 {
-
font-family: var(--font-serif);
-
text-transform: uppercase;
-
font-size: 0.875rem;
-
}
-
`;
-
-
useEffect(() => {
-
let isSubscribed = true;
-
-
const initializeUri = async () => {
-
if (propUri) {
-
setUri(propUri);
-
return;
-
}
-
-
if (author) {
-
try {
-
const currentUrl = window.location.href;
-
const apiUrl =
-
`https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=*&url=${
-
encodeURIComponent(
-
currentUrl,
-
)
-
}&author=${author}&sort=top`;
-
-
const response = await fetch(apiUrl);
-
const data = await response.json();
-
-
if (isSubscribed) {
-
if (data.posts && data.posts.length > 0) {
-
const post = data.posts[0];
-
setUri(post.uri);
-
} else {
-
setError("No matching post found");
-
if (onEmpty) {
-
onEmpty({
-
code: "not_found",
-
message: "No matching post found",
-
});
-
}
-
}
-
}
-
} catch (err) {
-
if (isSubscribed) {
-
setError("Error fetching post");
-
if (onEmpty) {
-
onEmpty({
-
code: "fetching_error",
-
message: "Error fetching post",
-
});
-
}
-
}
-
}
-
}
-
};
-
-
initializeUri();
-
-
return () => {
-
isSubscribed = false;
-
};
-
}, [propUri, author, onEmpty]);
-
-
useEffect(() => {
-
let isSubscribed = true;
-
-
const fetchThreadData = async () => {
-
if (!uri) return;
-
-
try {
-
const thread = await getPostThread(uri);
-
if (isSubscribed) {
-
setThread(thread);
-
}
-
} catch (err) {
-
if (isSubscribed) {
-
setError("Error loading comments");
-
if (onEmpty) {
-
onEmpty({
-
code: "comment_loading_error",
-
message: "Error loading comments",
-
});
-
}
-
}
-
}
-
};
-
-
fetchThreadData();
-
-
return () => {
-
isSubscribed = false;
-
};
-
}, [uri, onEmpty]);
-
-
const showMore = () => {
-
setVisibleCount((prevCount) => prevCount + 5);
-
};
-
-
if (!uri) return null;
-
-
if (error) {
-
return (
-
<div className="container">
-
<style>{styles}</style>
-
<p className="errorText">{error}</p>
-
</div>
-
);
-
}
-
-
if (!thread) {
-
return (
-
<div className="container">
-
<style>{styles}</style>
-
<p className="loadingText">Loading comments...</p>
-
</div>
-
);
-
}
-
-
let postUrl: string = uri;
-
if (uri.startsWith("at://")) {
-
const [, , did, _, rkey] = uri.split("/");
-
postUrl = `https://bsky.app/profile/${did}/post/${rkey}`;
-
}
-
-
if (!thread.replies || thread.replies.length === 0) {
-
return (
-
<div className="container">
-
<style>{styles}</style>
-
<PostSummary postUrl={postUrl} post={thread.post} />
-
</div>
-
);
-
}
-
-
// Safe sort - ensure we're working with valid objects
-
const sortedReplies = [...thread.replies].filter((reply) =>
-
AppBskyFeedDefs.isThreadViewPost(reply)
-
).sort(sortByLikes);
-
-
return (
-
<div className="container">
-
<style>{styles}</style>
-
<PostSummary postUrl={postUrl} post={thread.post} />
-
<hr className="divider" />
-
<div className="commentsList">
-
{sortedReplies.slice(0, visibleCount).map((reply) => {
-
if (!AppBskyFeedDefs.isThreadViewPost(reply)) return null;
-
return (
-
<Comment
-
key={reply.post.uri}
-
comment={reply}
-
filters={commentFilters}
-
/>
-
);
-
})}
-
{visibleCount < sortedReplies.length && (
-
<button onClick={showMore} className="showMoreButton">
-
Show more comments
-
</button>
-
)}
-
</div>
-
</div>
-
);
-
};
-
-
const getPostThread = async (
-
uri: string,
-
): Promise<AppBskyFeedDefs.ThreadViewPost> => {
-
const atUri = getAtUri(uri);
-
const params = new URLSearchParams({ uri: atUri });
-
-
const res = await fetch(
-
"https://public.api.bsky.app/xrpc/app.bsky.feed.getPostThread?" +
-
params.toString(),
-
{
-
method: "GET",
-
headers: {
-
Accept: "application/json",
-
},
-
cache: "no-store",
-
},
-
);
-
-
if (!res.ok) {
-
console.error(await res.text());
-
throw new Error("Failed to fetch post thread");
-
}
-
-
const data = (await res.json()) as AppBskyFeedGetPostThread.OutputSchema;
-
-
if (!AppBskyFeedDefs.isThreadViewPost(data.thread)) {
-
throw new Error("Could not find thread");
-
}
-
-
return data.thread;
-
};
-
-
const sortByLikes = (a: unknown, b: unknown) => {
-
if (
-
!AppBskyFeedDefs.isThreadViewPost(a) ||
-
!AppBskyFeedDefs.isThreadViewPost(b) ||
-
!("post" in a) ||
-
!("post" in b)
-
) {
-
return 0;
-
}
-
const aPost = a as AppBskyFeedDefs.ThreadViewPost;
-
const bPost = b as AppBskyFeedDefs.ThreadViewPost;
-
return (bPost.post.likeCount ?? 0) - (aPost.post.likeCount ?? 0);
-
};
+1 -1
routes/_404.tsx
···
<div class="flex-1 flex items-center justify-center">
<div class="p-8 pb-20 sm:p-20 text-center">
<Title class="font-serif-italic text-4xl sm:text-5xl lowercase mb-6">
-
Page not found.
+
Page not found
</Title>
<p class="my-4">The page you were looking for doesn't exist.</p>
<a href="/" class="underline">