Scrapboard.org client
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}