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