Leaflet Blog in Deno Fresh
1"use client";
2
3import { useEffect, useRef, useState } from "preact/hooks";
4import {
5 type PubLeafletBlocksText,
6 type PubLeafletDocument,
7} from "npm:@atcute/leaflet";
8
9import { cx } from "../lib/cx.ts";
10
11import { PostInfo } from "./post-info.tsx";
12import { Title } from "./typography.tsx";
13
14export function PostListItem({
15 post,
16 rkey,
17}: {
18 post: PubLeafletDocument.Main;
19 rkey: string;
20}) {
21 const [isHovered, setIsHovered] = useState(false);
22 const [isLeaving, setIsLeaving] = useState(false);
23 const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
24
25 // Clean up any timeouts on unmount
26 useEffect(() => {
27 return () => {
28 if (timeoutRef.current) {
29 clearTimeout(timeoutRef.current);
30 }
31 };
32 }, []);
33
34 const handleMouseEnter = () => {
35 if (timeoutRef.current) {
36 clearTimeout(timeoutRef.current);
37 }
38 setIsLeaving(false);
39 setIsHovered(true);
40 };
41
42 const handleMouseLeave = () => {
43 setIsLeaving(true);
44 timeoutRef.current = setTimeout(() => {
45 setIsHovered(false);
46 setIsLeaving(false);
47 }, 300); // Match animation duration
48 };
49
50 // Gather all text blocks' plaintext for preview and reading time
51 const allText = post.pages?.[0]?.blocks
52 ?.filter((block) => block.block.$type === "pub.leaflet.blocks.text")
53 .map((block) => (block.block as PubLeafletBlocksText.Main).plaintext)
54 .join(" ") || "";
55
56 return (
57 <>
58 {isHovered && (
59 <div
60 className={cx(
61 "fixed inset-0 pointer-events-none z-0",
62 isLeaving ? "animate-fade-out" : "animate-fade-in",
63 )}
64 >
65 <div className="h-full w-full pt-[120px] flex items-center overflow-hidden">
66 <div className="whitespace-nowrap animate-marquee font-serif font-medium uppercase leading-[0.8] text-[20vw] opacity-[0.015] -rotate-12 absolute left-0">
67 {Array(8).fill(post.title).join(" · ")}
68 </div>
69 </div>
70 </div>
71 )}
72 <a
73 href={`/post/${rkey}`}
74 className="w-full group block"
75 onMouseEnter={handleMouseEnter}
76 onMouseLeave={handleMouseLeave}
77 >
78 <article className="w-full flex flex-row border-b items-stretch relative transition-colors duration-300 ease-[cubic-bezier(0.33,0,0.67,1)] backdrop-blur-sm hover:bg-slate-700/5 dark:hover:bg-slate-200/10">
79 <div className="w-1.5 diagonal-pattern shrink-0 opacity-20 group-hover:opacity-100 transition-opacity duration-300 ease-[cubic-bezier(0.33,0,0.67,1)]" />
80 <div className="flex-1 py-2 px-4 z-10 relative w-full">
81 <Title className="text-lg w-full" level="h3">
82 {post.title}
83 </Title>
84 <PostInfo
85 content={allText}
86 createdAt={post.publishedAt}
87 className="text-xs mt-1 w-full"
88 />
89 <div className="grid transition-[grid-template-rows,opacity] duration-300 ease-[cubic-bezier(0.33,0,0.67,1)] grid-rows-[0fr] group-hover:grid-rows-[1fr] opacity-0 group-hover:opacity-100 mt-2">
90 <div className="overflow-hidden">
91 <p className="text-sm text-slate-600 dark:text-slate-300 break-words line-clamp-3">
92 {allText}
93 </p>
94 </div>
95 </div>
96 </div>
97 </article>
98 </a>
99 </>
100 );
101}