Scrapboard.org client
at main 98 lines 3.2 kB view raw
1"use client"; 2import { useBoardsStore } from "@/lib/stores/boards"; 3import { useAuth } from "@/lib/hooks/useAuth"; 4import { $Typed } from "@atproto/api"; 5import { AppBskyEmbedImages } from "@atproto/api/dist/client"; 6import { LoaderCircle } from "lucide-react"; 7import Image, { ImageProps } from "next/image"; 8import Link from "next/link"; 9import { motion } from "motion/react"; 10import Masonry from "react-masonry-css"; 11import { breakpointColumnsObj } from "@/components/Feed"; 12 13export const runtime = "edge"; 14 15function truncateString(str: string, num: number) { 16 return str.length > num ? str.slice(0, num) + "..." : str; 17} 18 19export default function BoardsPage() { 20 const { boards, isLoading, getBoards } = useBoardsStore(); 21 const { agent } = useAuth(); 22 23 if (!agent) return <div>Not logged in</div>; 24 const boardsFromDid = getBoards(agent.assertDid); 25 26 if (isLoading) 27 return ( 28 <div className="min-h-screen flex items-center justify-center px-4"> 29 <LoaderCircle className="animate-spin text-black/70 dark:text-white/70 w-8 h-8" /> 30 </div> 31 ); 32 if (!boardsFromDid || Object.entries(boardsFromDid).length <= 0) 33 return ( 34 <div className="min-h-screen flex items-center justify-center px-4"> 35 <p className="text-black/70 dark:text-white/70">No boards found</p> 36 </div> 37 ); 38 39 return ( 40 <div className="py-4 px-4 flex items-center justify-center"> 41 <div className="w-full max-w-4xl"> 42 <h1 className="font-medium text-lg mb-4">My Boards</h1> 43 <div className="grid grid-cols-1 md:grid-cols-4 gap-3"> 44 {Array.from(Object.entries(boardsFromDid)).map(([key, it]) => ( 45 <Link 46 href={`/board/${agent?.did ?? "unknown"}/${key}`} 47 key={key} 48 className="h-full" 49 > 50 <motion.div 51 initial={{ opacity: 0, y: 2, filter: "blur(8px)" }} 52 animate={{ opacity: 1, y: 0, filter: "blur(0px)" }} 53 transition={{ duration: 0.3, ease: "easeOut" }} 54 whileTap={{ scale: 0.95 }} 55 className="flex flex-col h-full bg-black/10 dark:bg-white/3 p-4 rounded-lg hover:bg-black/15 dark:hover:bg-white/5 transition-colors" 56 > 57 <h2 className="font-medium text-lg">{it.name}</h2> 58 <p className="text-sm text-black/80 dark:text-white/80 mt-1 line-clamp-2"> 59 {it.description} 60 </p> 61 </motion.div> 62 </Link> 63 ))} 64 </div> 65 </div> 66 </div> 67 ); 68} 69 70type BskyImageProps = { 71 embed: 72 | $Typed<AppBskyEmbedImages.View> 73 | { 74 $type: string; 75 } 76 | undefined; 77 imageIndex?: number; 78 className?: string; 79 width?: number; 80 height?: number; 81} & Omit<ImageProps, "src" | "alt">; 82 83function BskyImage({ embed, imageIndex = 0, ...props }: BskyImageProps) { 84 if (!AppBskyEmbedImages.isView(embed)) return null; 85 86 const image = embed.images?.[imageIndex]; 87 if (!image) return null; 88 89 return ( 90 <Image 91 src={image.fullsize} 92 alt={image.alt || "Post Image"} 93 placeholder="blur" 94 blurDataURL={image.thumb} 95 {...props} 96 /> 97 ); 98}