alternative tangled frontend (extremely wip)

feat: dropdown modal

serenity 61c09feb bbad2d0c

+79 -7
+48
src/components/Dropdown/DropdownModal.tsx
··· 1 + import { useModalEscapeEffect } from "@/lib/hooks/useModalEscapeEffect"; 2 + import { useModalMousedownEffect } from "@/lib/hooks/useModalMousedownEffect"; 3 + import { AnimatePresence, motion } from "motion/react"; 4 + import { ReactNode, useRef, useState } from "react"; 5 + 6 + export const DropdownModal = ({ 7 + buttonComponent, 8 + children, 9 + className, 10 + }: { 11 + buttonComponent: ReactNode; 12 + children: ReactNode; 13 + className?: string; 14 + }) => { 15 + const [showDropdown, setShowDropdown] = useState(false); 16 + 17 + const dropdownRef = useRef<HTMLDivElement>(null); 18 + 19 + useModalEscapeEffect({ setShowModal: setShowDropdown }); 20 + useModalMousedownEffect({ 21 + setShowModal: setShowDropdown, 22 + modalRef: dropdownRef, 23 + }); 24 + 25 + return ( 26 + <div ref={dropdownRef} className="relative inline-block"> 27 + <button 28 + className="cursor-pointer" 29 + onClick={() => setShowDropdown((prev) => !prev)} 30 + > 31 + {buttonComponent} 32 + </button> 33 + <AnimatePresence> 34 + {showDropdown && ( 35 + <motion.div 36 + initial={{ opacity: 0, x: 8 }} 37 + animate={{ opacity: 1, x: 0 }} 38 + exit={{ opacity: 0, x: 8 }} 39 + transition={{ duration: 0.1, ease: "easeOut" }} 40 + className={`absolute right-0 z-50 origin-top-right cursor-default ${className}`} 41 + > 42 + {children} 43 + </motion.div> 44 + )} 45 + </AnimatePresence> 46 + </div> 47 + ); 48 + };
+31 -7
src/components/Nav/NavBarAuthed.tsx
··· 1 1 import { UnderlineIconRouterLink } from "@/components/Animated/UnderlineIconRouterLink"; 2 + import { DropdownModal } from "@/components/Dropdown/DropdownModal"; 2 3 import { StrandIcon } from "@/components/Icons/Branding/StrandIcon"; 3 4 import { Loading } from "@/components/Icons/Loading"; 4 - import { LucideLogIn } from "@/components/Icons/LucideLogIn"; 5 + import { Avatar } from "@/components/Profile/Avatar"; 6 + import { useModalEscapeEffect } from "@/lib/hooks/useModalEscapeEffect"; 7 + import { useModalMousedownEffect } from "@/lib/hooks/useModalMousedownEffect"; 5 8 import { useOAuth } from "@/lib/oauth"; 6 9 import { useAvatarQuery } from "@/lib/queries/get-avatar"; 7 - import { err } from "@/lib/result"; 10 + import { AnimatePresence, motion } from "motion/react"; 11 + import { useEffect, useRef, useState } from "react"; 8 12 9 13 export const NavBarAuthed = () => { 10 14 const oauth = useOAuth(); ··· 17 21 error: avatarQueryError, 18 22 } = useAvatarQuery({ did, repoUrl }); 19 23 20 - const avatarSrc = avatarQueryData === "#" ? undefined : avatarQueryData; 24 + const avatarUri = avatarQueryData === "#" ? undefined : avatarQueryData; 21 25 22 26 return ( 23 27 <div className="bg-surface0 flex w-full items-center justify-between p-3"> ··· 39 43 ) : avatarQueryError ? ( 40 44 <p>{avatarQueryError.message}</p> 41 45 ) : ( 42 - <img 43 - src={avatarSrc} 44 - className="outline-accent h-10 rounded-full outline" 45 - /> 46 + <AvatarWithDropdown uri={avatarUri} /> 46 47 )} 47 48 </button> 48 49 </div> 49 50 </div> 50 51 ); 51 52 }; 53 + 54 + const AvatarWithDropdown = ({ uri }: { uri: string | undefined }) => { 55 + const [showDropdown, setShowDropdown] = useState(false); 56 + 57 + const dropdownRef = useRef<HTMLDivElement>(null); 58 + 59 + useModalEscapeEffect({ setShowModal: setShowDropdown }); 60 + useModalMousedownEffect({ 61 + setShowModal: setShowDropdown, 62 + modalRef: dropdownRef, 63 + }); 64 + 65 + const AvatarButton = <Avatar uri={uri} />; 66 + 67 + return ( 68 + <DropdownModal 69 + buttonComponent={AvatarButton} 70 + className="bg-surface0 mt-3.5 w-72 rounded-lg p-2" 71 + > 72 + <p>Hello</p> 73 + </DropdownModal> 74 + ); 75 + };