this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: Some nice changes, including:

- back to top button
- show more/less users,
- links to stat buttons,
- metadata changes, and more.

scanash00 fd090b66 d82f8652

+191 -86
+2 -1
README.md
··· 1 1 # Better PDS Dash 2 + 2 3 ![screenshot](screenshot.png) 3 4 A comic-style Bluesky PDS dashboard with full support for Bluesky's embeds and media. 4 5 ··· 44 45 45 46 Uh, you will figure it out, just use Traefik/Caddy to serve this on / 46 47 47 - Here's a guide that might be helpful: [Setting a custom homepage on a PDS](https://willdot.leaflet.pub/3m25uvnuwnk2t) 48 + Here's a guide that might be helpful: [Setting a custom homepage on a PDS](https://willdot.leaflet.pub/3m25uvnuwnk2t)
src/app/favicon.ico

This is a binary file and will not be displayed.

+2
src/app/icon.svg
··· 1 + <?xml version='1.0' encoding='utf-8'?> 2 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 419.14 403.6"> <path d="m147.66 24.808c-0.38417 0.005051-0.77826 0.02449-1.1698 0.056129-15.592 2.777-24.166 19.789-25.408 34.791-1.8153 13.613 3.9987 31.109 18.485 34.159 2.1012 0.41275 4.2845 0.22411 6.3384-0.35088 17.444-6.0351 25.25-27.351 22.729-45.008-0.73297-11.404-9.0645-23.804-20.974-23.648zm-66.105 0.7298c-9.6953 0.24557-17.248 10.391-18.784 19.928-3.2387 18.895 5.0918 42.373 24.17 48.348 1.787 0.40099 3.6355 0.44228 5.4407 0.15437 12.895-2.2174 18.358-17.135 17.818-29.177-0.38811-15.886-8.3856-34.254-24.374-38.72-1.4615-0.39988-2.8859-0.56838-4.271-0.5333zm108.35 53.667c-16.772 1.0353-28.271 18.302-29.91 34.426-2.2196 11.629 3.7888 27.182 16.649 28.11 17.931-0.22806 30.164-19.998 30.672-37.008 1.1342-11.25-5.2888-24.907-17.41-25.528zm-146.8 3.8313c-7.1356-0.063339-14.154 4.3467-16.485 11.831-6.221 20.02 6.2716 45.376 26.823 49.583 8.577 1.5144 16.113-5.2817 18.24-13.459 4.5069-17.49-4.7501-37.798-20.416-45.737-2.5523-1.4779-5.3689-2.1926-8.1611-2.2174zm75.191 35.899c-13.989 0.030777-28.197 7.3553-37.255 18.258-9.7834 12.708-17.798 27.496-20.933 43.534-3.8091 11.545 4.8241 24.83 16.785 24.911 14.389 0.41242 26.756-9.7826 41.064-10.54 12.139-1.9094 23.009 4.6055 33.923 8.8696 9.2924 4.1008 22.133 2.5293 26.877-7.789 3.7317-10.517-1.761-21.732-6.3112-31.072-9.2806-15.16-19.223-31.093-34.44-40.559-6.0992-3.9007-12.877-5.6287-19.709-5.6137z" fill="#ffd1dc"/> <path d="m324.66 195.81c-0.38416 0.005051-0.77826 0.02449-1.1697 0.056137-15.592 2.777-24.166 19.789-25.408 34.791-1.8153 13.613 3.9988 31.109 18.485 34.159 2.1012 0.41272 4.2845 0.22409 6.3384-0.35089 17.444-6.0351 25.25-27.351 22.729-45.008-0.73297-11.404-9.0645-23.804-20.974-23.648zm-66.105 0.7298c-9.6953 0.24556-17.248 10.391-18.784 19.928-3.2387 18.895 5.0918 42.373 24.17 48.348 1.787 0.40097 3.6355 0.44226 5.4407 0.15436 12.895-2.2174 18.358-17.135 17.818-29.177-0.38809-15.886-8.3856-34.254-24.374-38.72-1.4615-0.39989-2.8859-0.56839-4.271-0.53331zm108.35 53.667c-16.772 1.0353-28.271 18.302-29.91 34.426-2.2196 11.629 3.7888 27.182 16.649 28.11 17.931-0.22806 30.164-19.998 30.672-37.008 1.1342-11.25-5.2888-24.907-17.41-25.528zm-146.8 3.8313c-7.1356-0.063339-14.154 4.3467-16.485 11.831-6.221 20.02 6.2716 45.376 26.823 49.583 8.577 1.5144 16.113-5.2817 18.24-13.459 4.5069-17.49-4.7501-37.798-20.416-45.737-2.5523-1.4779-5.3689-2.1926-8.1611-2.2174zm75.191 35.899c-13.989 0.030762-28.197 7.3553-37.255 18.258-9.7835 12.708-17.798 27.496-20.933 43.534-3.8091 11.545 4.8241 24.83 16.785 24.911 14.389 0.41242 26.756-9.7826 41.064-10.54 12.139-1.9094 23.009 4.6055 33.923 8.8696 9.2924 4.1008 22.133 2.5293 26.877-7.789 3.7317-10.517-1.761-21.732-6.3112-31.072-9.2806-15.16-19.223-31.093-34.44-40.559-6.0992-3.9007-12.877-5.6287-19.709-5.6136z" fill="#ffd1dc"/> </svg>
+2 -2
src/app/layout.tsx
··· 13 13 }); 14 14 15 15 export const metadata: Metadata = { 16 - title: 'PDS Dash', 17 - description: 'Bluesky PDS Dashboard', 16 + title: 'Atproto PDS', 17 + description: 'This is an AT Protocol Personal Data Server (aka, an atproto PDS)', 18 18 }; 19 19 20 20 export default function RootLayout({
+13 -4
src/app/page.tsx
··· 2 2 import {UserList} from '@/components/UserList'; 3 3 import {PostFeed} from '@/components/PostFeed'; 4 4 5 + import {BackToTop} from '@/components/BackToTop'; 6 + 5 7 export const dynamic = 'force-dynamic'; 6 8 7 9 export default async function Home() { ··· 24 26 25 27 return ( 26 28 <main className="min-h-screen halftone p-4 md:p-8 relative z-10"> 29 + <BackToTop /> 27 30 <div className="max-w-6xl mx-auto space-y-6"> 28 31 <header className="comic-panel p-8 text-center mb-8 bg-yellow-200"> 29 - <h1 30 - className="text-4xl md:text-6xl font-bold transform -rotate-1" 31 - style={{fontFamily: 'Bangers, cursive'}} 32 - > 32 + <h1 className="text-4xl md:text-6xl font-bold" style={{fontFamily: 'Bangers, cursive'}}> 33 33 <span className="text-red-500">{pdsHostname.toUpperCase()}</span> 34 34 </h1> 35 + <div className="mt-6 text-lg font-bold space-y-1"> 36 + <p>This is an AT Protocol Personal Data Server (aka, an atproto PDS)</p> 37 + <p className="text-sm text-zinc-700"> 38 + Most API routes are under{' '} 39 + <code className="bg-white px-2 py-0.5 rounded border-2 border-black font-mono text-black"> 40 + /xrpc/ 41 + </code> 42 + </p> 43 + </div> 35 44 </header> 36 45 37 46 <section className="comic-panel p-6">
+46
src/components/BackToTop.tsx
··· 1 + 'use client'; 2 + 3 + import {useState, useEffect} from 'react'; 4 + import {ArrowUpIcon} from '@heroicons/react/24/solid'; 5 + 6 + export function BackToTop() { 7 + const [isVisible, setIsVisible] = useState(false); 8 + 9 + const toggleVisibility = () => { 10 + if (window.scrollY > 300) { 11 + setIsVisible(true); 12 + } else { 13 + setIsVisible(false); 14 + } 15 + }; 16 + 17 + const scrollToTop = () => { 18 + window.scrollTo({ 19 + top: 0, 20 + behavior: 'smooth', 21 + }); 22 + }; 23 + 24 + useEffect(() => { 25 + window.addEventListener('scroll', toggleVisibility); 26 + return () => { 27 + window.removeEventListener('scroll', toggleVisibility); 28 + }; 29 + }, []); 30 + 31 + if (!isVisible) { 32 + return null; 33 + } 34 + 35 + return ( 36 + <div className="sticky top-[85vh] w-full flex justify-end pointer-events-none z-50"> 37 + <button 38 + onClick={scrollToTop} 39 + className="pointer-events-auto mr-4 comic-button bg-yellow-400 p-3 rounded-full shadow-lg hover:bg-yellow-300 transition-all transform hover:scale-110" 40 + aria-label="Back to top" 41 + > 42 + <ArrowUpIcon className="w-6 h-6 text-black" /> 43 + </button> 44 + </div> 45 + ); 46 + }
+21 -6
src/components/PostFeed.tsx
··· 510 510 511 511 <div className="flex items-center gap-4 text-sm"> 512 512 {post.replyCount !== undefined && ( 513 - <div className="flex items-center gap-1 px-3 py-1 bg-blue-200 border-2 border-black rounded-full font-bold"> 513 + <a 514 + href={`https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('/').pop()}`} 515 + target="_blank" 516 + rel="noopener noreferrer" 517 + className="flex items-center gap-1 px-3 py-1 bg-blue-200 border-2 border-black rounded-full font-bold hover:bg-blue-300 transition-colors" 518 + > 514 519 <ChatBubbleOvalLeftIcon className="w-4 h-4" /> 515 520 <span>{post.replyCount}</span> 516 - </div> 521 + </a> 517 522 )} 518 523 {post.repostCount !== undefined && ( 519 - <div className="flex items-center gap-1 px-3 py-1 bg-green-200 border-2 border-black rounded-full font-bold"> 524 + <a 525 + href={`https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('/').pop()}`} 526 + target="_blank" 527 + rel="noopener noreferrer" 528 + className="flex items-center gap-1 px-3 py-1 bg-green-200 border-2 border-black rounded-full font-bold hover:bg-green-300 transition-colors" 529 + > 520 530 <ArrowPathIcon className="w-4 h-4" /> 521 531 <span>{post.repostCount}</span> 522 - </div> 532 + </a> 523 533 )} 524 534 {post.likeCount !== undefined && ( 525 - <div className="flex items-center gap-1 px-3 py-1 bg-red-200 border-2 border-black rounded-full font-bold"> 535 + <a 536 + href={`https://bsky.app/profile/${post.author.handle}/post/${post.uri.split('/').pop()}`} 537 + target="_blank" 538 + rel="noopener noreferrer" 539 + className="flex items-center gap-1 px-3 py-1 bg-red-200 border-2 border-black rounded-full font-bold hover:bg-red-300 transition-colors" 540 + > 526 541 <HeartIcon className="w-4 h-4" /> 527 542 <span>{post.likeCount}</span> 528 - </div> 543 + </a> 529 544 )} 530 545 </div> 531 546 </div>
+102 -70
src/components/UserList.tsx
··· 1 + 'use client'; 2 + 3 + import {useState} from 'react'; 1 4 import Image from 'next/image'; 2 5 import {UserProfile} from '@/lib/atproto'; 6 + import {ChevronDownIcon, ChevronUpIcon} from '@heroicons/react/24/solid'; 3 7 4 8 interface UserListProps { 5 9 users: UserProfile[]; 6 10 } 7 11 8 12 export function UserList({users}: UserListProps) { 13 + const [isExpanded, setIsExpanded] = useState(false); 14 + const INITIAL_COUNT = 8; 15 + const displayedUsers = isExpanded ? users : users.slice(0, INITIAL_COUNT); 16 + const hasMore = users.length > INITIAL_COUNT; 17 + 9 18 const colors = [ 10 19 'bg-red-400', 11 20 'bg-blue-400', ··· 18 27 ]; 19 28 20 29 return ( 21 - <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> 22 - {users.map((user, index) => { 23 - const isInvalidHandle = user.handle.endsWith('.invalid') || user.handle === user.did; 24 - const profileUrl = isInvalidHandle 25 - ? `https://bsky.app/profile/${user.did}` 26 - : `https://bsky.app/profile/${user.handle}`; 27 - const avatarInitial = isInvalidHandle ? '⚠️' : user.handle[0]?.toUpperCase() || '?'; 30 + <div className="space-y-6"> 31 + <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4"> 32 + {displayedUsers.map((user, index) => { 33 + const isInvalidHandle = user.handle.endsWith('.invalid') || user.handle === user.did; 34 + const profileUrl = isInvalidHandle 35 + ? `https://bsky.app/profile/${user.did}` 36 + : `https://bsky.app/profile/${user.handle}`; 37 + const avatarInitial = isInvalidHandle ? '⚠️' : user.handle[0]?.toUpperCase() || '?'; 28 38 29 - return ( 30 - <a 31 - key={user.did} 32 - href={profileUrl} 33 - target="_blank" 34 - rel="noopener noreferrer" 35 - className="comic-panel interactive-panel p-4 flex flex-col h-full" 36 - > 37 - <div className="flex items-center space-x-3"> 38 - {user.avatar ? ( 39 - <Image 40 - src={user.avatar} 41 - alt={isInvalidHandle ? 'Invalid Handle' : user.handle} 42 - width={48} 43 - height={48} 44 - className="w-12 h-12 rounded-full border-3 border-black" 45 - style={{border: '3px solid #000'}} 46 - unoptimized={!user.avatar.includes('cdn.bsky.app')} 47 - /> 48 - ) : ( 49 - <div 50 - className={`w-12 h-12 rounded-full flex items-center justify-center text-white font-bold border-3 border-black ${colors[index % colors.length]}`} 51 - style={{border: '3px solid #000'}} 52 - > 53 - <span className="text-xl">{avatarInitial}</span> 39 + return ( 40 + <a 41 + key={user.did} 42 + href={profileUrl} 43 + target="_blank" 44 + rel="noopener noreferrer" 45 + className="comic-panel interactive-panel p-4 flex flex-col h-full" 46 + > 47 + <div className="flex items-center space-x-3"> 48 + {user.avatar ? ( 49 + <Image 50 + src={user.avatar} 51 + alt={isInvalidHandle ? 'Invalid Handle' : user.handle} 52 + width={48} 53 + height={48} 54 + className="w-12 h-12 rounded-full border-3 border-black" 55 + style={{border: '3px solid #000'}} 56 + unoptimized={!user.avatar.includes('cdn.bsky.app')} 57 + /> 58 + ) : ( 59 + <div 60 + className={`w-12 h-12 rounded-full flex items-center justify-center text-white font-bold border-3 border-black ${colors[index % colors.length]}`} 61 + style={{border: '3px solid #000'}} 62 + > 63 + <span className="text-xl">{avatarInitial}</span> 64 + </div> 65 + )} 66 + <div className="overflow-hidden flex-1"> 67 + <p className="text-sm font-bold text-black truncate flex items-center gap-1"> 68 + <span>{user.displayName || user.handle}</span> 69 + {!isInvalidHandle && 70 + user.verified && 71 + (user.trustedVerifier ? ( 72 + <svg className="w-3.5 h-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24"> 73 + <path 74 + fill="#1185FE" 75 + d="M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.15 4.15 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.15 4.15 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.15 4.15 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.15 4.15 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.15 4.15 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.15 4.15 0 0 0 .776-3.404 4.154 4.154 0 0 1 4-5.017 4.15 4.15 0 0 0 3.146-1.515Z" 76 + /> 77 + <path 78 + fill="#fff" 79 + fillRule="evenodd" 80 + d="M17.861 8.26a1.44 1.44 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z" 81 + clipRule="evenodd" 82 + /> 83 + </svg> 84 + ) : ( 85 + <svg className="w-3.5 h-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24"> 86 + <circle cx="12" cy="12" r="11.5" fill="#1183FE" /> 87 + <path 88 + fill="#fff" 89 + fillRule="evenodd" 90 + d="M17.659 8.175a1.36 1.36 0 0 1 0 1.925l-6.224 6.223a1.36 1.36 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.36 1.36 0 0 1 1.925 0Z" 91 + clipRule="evenodd" 92 + /> 93 + </svg> 94 + ))} 95 + </p> 96 + <p className="text-xs text-zinc-600 truncate"> 97 + {isInvalidHandle ? '⚠️ Invalid Handle' : `@${user.handle}`} 98 + </p> 54 99 </div> 55 - )} 56 - <div className="overflow-hidden flex-1"> 57 - <p className="text-sm font-bold text-black truncate flex items-center gap-1"> 58 - <span>{user.displayName || user.handle}</span> 59 - {!isInvalidHandle && 60 - user.verified && 61 - (user.trustedVerifier ? ( 62 - <svg className="w-3.5 h-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24"> 63 - <path 64 - fill="#1185FE" 65 - d="M8.792 1.615a4.154 4.154 0 0 1 6.416 0 4.15 4.15 0 0 0 3.146 1.515 4.154 4.154 0 0 1 4 5.017 4.15 4.15 0 0 0 .777 3.404 4.154 4.154 0 0 1-1.427 6.255 4.15 4.15 0 0 0-2.177 2.73 4.154 4.154 0 0 1-5.781 2.784 4.15 4.15 0 0 0-3.492 0 4.154 4.154 0 0 1-5.78-2.784 4.15 4.15 0 0 0-2.178-2.73A4.154 4.154 0 0 1 .87 11.551a4.15 4.15 0 0 0 .776-3.404 4.154 4.154 0 0 1 4-5.017 4.15 4.15 0 0 0 3.146-1.515Z" 66 - /> 67 - <path 68 - fill="#fff" 69 - fillRule="evenodd" 70 - d="M17.861 8.26a1.44 1.44 0 0 1 0 2.033l-6.571 6.571a1.437 1.437 0 0 1-2.033 0L5.97 13.58a1.438 1.438 0 0 1 2.033-2.033l2.27 2.269 5.554-5.555a1.437 1.437 0 0 1 2.033 0Z" 71 - clipRule="evenodd" 72 - /> 73 - </svg> 74 - ) : ( 75 - <svg className="w-3.5 h-3.5 flex-shrink-0" fill="none" viewBox="0 0 24 24"> 76 - <circle cx="12" cy="12" r="11.5" fill="#1183FE" /> 77 - <path 78 - fill="#fff" 79 - fillRule="evenodd" 80 - d="M17.659 8.175a1.36 1.36 0 0 1 0 1.925l-6.224 6.223a1.36 1.36 0 0 1-1.925 0L6.4 13.212a1.361 1.361 0 0 1 1.925-1.925l2.149 2.148 5.26-5.26a1.36 1.36 0 0 1 1.925 0Z" 81 - clipRule="evenodd" 82 - /> 83 - </svg> 84 - ))} 85 - </p> 86 - <p className="text-xs text-zinc-600 truncate"> 87 - {isInvalidHandle ? '⚠️ Invalid Handle' : `@${user.handle}`} 88 - </p> 89 100 </div> 90 - </div> 91 - </a> 92 - ); 93 - })} 101 + </a> 102 + ); 103 + })} 104 + </div> 105 + 106 + {hasMore && ( 107 + <div className="flex justify-center"> 108 + <button 109 + onClick={() => setIsExpanded(!isExpanded)} 110 + className="comic-button bg-white px-6 py-2 flex items-center gap-2 hover:bg-zinc-50" 111 + > 112 + {isExpanded ? ( 113 + <> 114 + <span>Show Less</span> 115 + <ChevronUpIcon className="w-5 h-5" /> 116 + </> 117 + ) : ( 118 + <> 119 + <span>Show All Users ({users.length})</span> 120 + <ChevronDownIcon className="w-5 h-5" /> 121 + </> 122 + )} 123 + </button> 124 + </div> 125 + )} 94 126 </div> 95 127 ); 96 128 }
+3 -3
src/lib/atproto.ts
··· 110 110 let isVerified = false; 111 111 let isTrustedVerifier = false; 112 112 113 - if ((profile as any).verification) { 114 - const verification = (profile as any).verification as BskyVerification; 113 + if ((profile as {verification?: unknown}).verification) { 114 + const verification = (profile as {verification?: unknown}).verification as BskyVerification; 115 115 isVerified = 116 116 verification.verifiedStatus === 'valid' || 117 117 (verification.verifications?.some(v => v.isValid === true) ?? false); ··· 268 268 }; 269 269 post.quotedPost = { 270 270 uri: quotedRecord.uri, 271 - author: {did: '', handle: '', verified: false, trustedVerifier: false}, // Will fetch later 271 + author: {did: '', handle: '', verified: false, trustedVerifier: false}, 272 272 text: '', 273 273 createdAt: '', 274 274 };