an app to share curated trails sidetrail.app
1

Configure Feed

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

at main 2.2 kB View raw
1"use client"; 2 3import Link from "next/link"; 4import "./FloatingAvatar.css"; 5 6interface FloatingAvatarProps { 7 src?: string; 8 handle: string; 9 title: string; 10 contained?: boolean; 11 opaque?: boolean; 12 noLink?: boolean; 13} 14 15export function FloatingAvatar({ 16 src, 17 handle, 18 title, 19 contained = false, 20 opaque = false, 21 noLink = false, 22}: FloatingAvatarProps) { 23 if (!src) return null; 24 25 // Simple string hash function (deterministic across server/client) 26 const hashString = (str: string) => { 27 let hash = 0; 28 for (let i = 0; i < str.length; i++) { 29 hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0; 30 } 31 return Math.abs(hash); 32 }; 33 34 // Use handle for seeding - same handle always gets same animation 35 const baseHash = hashString(handle); 36 37 // Generate stable random values based on hash 38 const random = (offset: number) => { 39 // Simple LCG (Linear Congruential Generator) for deterministic randomness 40 const a = 1664525; 41 const c = 1013904223; 42 const m = 2 ** 32; 43 const value = ((baseHash + offset) * a + c) % m; 44 return value / m; 45 }; 46 47 // More varied random animation duration (3s to 9s for bigger range) 48 const duration = 3 + random(0) * 6; 49 // Random animation delay (0s to 4s for more spread) 50 const delay = random(1) * 4; 51 // Random timing function index 52 const timingFunctions = ["ease-in-out", "ease-in", "ease-out", "linear"]; 53 const timingFunction = timingFunctions[Math.floor(random(2) * timingFunctions.length)]; 54 55 const imgElement = ( 56 <img 57 src={src} 58 alt={handle} 59 title={title} 60 className={`FloatingAvatar ${contained ? "FloatingAvatar-contained" : ""} ${opaque ? "FloatingAvatar-opaque" : ""} ${noLink ? "" : "FloatingAvatar-clickable"}`} 61 style={{ 62 animationDuration: `${duration}s`, 63 animationDelay: `${delay}s`, 64 animationTimingFunction: timingFunction, 65 }} 66 /> 67 ); 68 69 if (noLink) { 70 return imgElement; 71 } 72 73 return ( 74 <Link 75 href={`/@${handle}/walking`} 76 prefetch={false} 77 onClick={(e) => e.stopPropagation()} 78 target="_blank" 79 rel="noopener noreferrer" 80 className="FloatingAvatar-link" 81 tabIndex={-1} 82 > 83 {imgElement} 84 </Link> 85 ); 86}