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}