Leaflet Blog in Deno Fresh
at main 3.4 kB view raw
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}