Retro Bulletin Board Systems on atproto. Web app and TUI. lazy mirror of alyraffauf/atbbs atbbs.xyz
forums python tui atproto bbs
3
fork

Configure Feed

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

web: put post actions in responsive menu

+76 -21
+69 -16
web/src/components/post/PostActions.tsx
··· 1 - const actionStyle = "text-xs text-neutral-400 hover:text-red-400"; 1 + import { useRef, useState, useEffect } from "react"; 2 + import { Quote, MoreHorizontal, Trash2, Ban, EyeOff } from "lucide-react"; 2 3 3 4 interface PostActionsProps { 4 5 isAuthor: boolean; ··· 17 18 onHide, 18 19 onQuote, 19 20 }: PostActionsProps) { 21 + const [open, setOpen] = useState(false); 22 + const menuRef = useRef<HTMLDivElement>(null); 23 + 24 + useEffect(() => { 25 + if (!open) return; 26 + function onClickOutside(e: MouseEvent) { 27 + if (menuRef.current && !menuRef.current.contains(e.target as Node)) { 28 + setOpen(false); 29 + } 30 + } 31 + document.addEventListener("mousedown", onClickOutside); 32 + return () => document.removeEventListener("mousedown", onClickOutside); 33 + }, [open]); 34 + 35 + const canDelete = isAuthor && !!onDelete; 36 + const canBan = isSysop && !isAuthor && !!onBan; 37 + const canHide = isSysop && !!onHide; 38 + const hasModActions = canDelete || canBan || canHide; 39 + 40 + if (!onQuote && !hasModActions) return null; 41 + 42 + function select(action: () => void) { 43 + setOpen(false); 44 + action(); 45 + } 46 + 47 + const menuItem = 48 + "flex items-center gap-1.5 w-full px-3 py-1.5 text-xs text-neutral-400 hover:bg-neutral-800"; 49 + const dangerItem = menuItem + " hover:text-red-400"; 50 + 20 51 return ( 21 - <span className="reply-actions flex items-center gap-3"> 22 - {onQuote && ( 23 - <button onClick={onQuote} className="text-xs text-neutral-400 hover:text-neutral-300"> 24 - quote 25 - </button> 26 - )} 27 - {isAuthor && onDelete && ( 28 - <button onClick={onDelete} className={actionStyle}>delete</button> 29 - )} 30 - {isSysop && !isAuthor && onBan && ( 31 - <button onClick={onBan} className={actionStyle}>ban</button> 32 - )} 33 - {isSysop && onHide && ( 34 - <button onClick={onHide} className={actionStyle}>hide</button> 52 + <div className="relative post-actions" ref={menuRef}> 53 + <button 54 + onClick={() => setOpen(!open)} 55 + className="text-neutral-400 hover:text-neutral-300" 56 + > 57 + <MoreHorizontal size={16} /> 58 + </button> 59 + 60 + {open && ( 61 + <div className="absolute right-0 mt-1 bg-neutral-900 border border-neutral-800 rounded shadow-lg z-10 py-1 min-w-28"> 62 + {onQuote && ( 63 + <button onClick={() => select(onQuote)} className={menuItem}> 64 + <Quote size={12} /> quote 65 + </button> 66 + )} 67 + 68 + {onQuote && hasModActions && ( 69 + <div className="border-t border-neutral-800 my-1" /> 70 + )} 71 + 72 + {canDelete && ( 73 + <button onClick={() => select(onDelete)} className={dangerItem}> 74 + <Trash2 size={12} /> delete 75 + </button> 76 + )} 77 + {canBan && ( 78 + <button onClick={() => select(onBan)} className={dangerItem}> 79 + <Ban size={12} /> ban 80 + </button> 81 + )} 82 + {canHide && ( 83 + <button onClick={() => select(onHide)} className={dangerItem}> 84 + <EyeOff size={12} /> hide 85 + </button> 86 + )} 87 + </div> 35 88 )} 36 - </span> 89 + </div> 37 90 ); 38 91 }
+7 -5
web/src/index.css
··· 42 42 monospace; 43 43 } 44 44 45 - .reply-actions { 46 - display: none; 47 - } 45 + @media (hover: hover) { 46 + .post-actions { 47 + visibility: hidden; 48 + } 48 49 49 - .reply-card:hover .reply-actions { 50 - display: inline-flex; 50 + .reply-card:hover .post-actions { 51 + visibility: visible; 52 + } 51 53 } 52 54 53 55 @keyframes atbbs-progress {