Teal.fm frontend powered by slices.network tealfm-slices.wisp.place
tealfm slices
at main 3.9 kB view raw
1import { graphql, useLazyLoadQuery } from "react-relay"; 2import { Link, NavLink, useParams } from "react-router-dom"; 3import { useMemo, type PropsWithChildren } from "react"; 4import type { ProfileLayoutQuery as ProfileLayoutQueryType } from "./__generated__/ProfileLayoutQuery.graphql"; 5import ScrobbleChart from "./ScrobbleChart"; 6 7export default function ProfileLayout({ children }: PropsWithChildren) { 8 const { handle } = useParams<{ handle: string }>(); 9 10 const queryVariables = useMemo(() => { 11 // Round to start of day to keep timestamp stable 12 const now = new Date(); 13 now.setHours(0, 0, 0, 0); 14 const ninetyDaysAgo = new Date(now.getTime() - 90 * 24 * 60 * 60 * 1000); 15 16 return { 17 where: { actorHandle: { eq: handle } }, 18 chartWhere: { 19 actorHandle: { eq: handle }, 20 playedTime: { 21 gte: ninetyDaysAgo.toISOString(), 22 }, 23 }, 24 }; 25 }, [handle]); 26 27 const queryData = useLazyLoadQuery<ProfileLayoutQueryType>( 28 graphql` 29 query ProfileLayoutQuery($where: FmTealAlphaFeedPlayWhereInput!, $chartWhere: FmTealAlphaFeedPlayWhereInput!) { 30 ...ScrobbleChart_data 31 fmTealAlphaFeedPlays( 32 first: 1 33 sortBy: [{ field: playedTime, direction: desc }] 34 where: $where 35 ) { 36 edges { 37 node { 38 actorHandle 39 appBskyActorProfile { 40 displayName 41 description 42 avatar { 43 url(preset: "avatar") 44 } 45 } 46 } 47 } 48 } 49 } 50 `, 51 queryVariables 52 ); 53 54 const profile = queryData?.fmTealAlphaFeedPlays?.edges?.[0]?.node?.appBskyActorProfile; 55 56 return ( 57 <div className="min-h-screen bg-zinc-950 text-zinc-300 font-mono"> 58 <div className="max-w-4xl mx-auto px-6 py-12"> 59 <Link 60 to="/" 61 className="px-2 py-1 text-xs text-zinc-500 hover:text-zinc-300 transition-colors inline-block mb-8" 62 > 63 Back 64 </Link> 65 66 <div className="mb-12 border-b border-zinc-800 pb-6 relative"> 67 <div className="absolute inset-0 pointer-events-none opacity-40"> 68 <ScrobbleChart queryRef={queryData} /> 69 </div> 70 <div className="relative flex items-start gap-6"> 71 {profile?.avatar?.url && ( 72 <img 73 src={profile.avatar.url} 74 alt={profile.displayName ?? handle ?? "User"} 75 className="w-16 h-16 flex-shrink-0 object-cover" 76 /> 77 )} 78 <div className="flex-1"> 79 <h1 className="text-lg font-medium mb-1 text-zinc-100"> 80 {profile?.displayName ?? handle} 81 </h1> 82 <p className="text-xs text-zinc-500 mb-2">@{handle}</p> 83 {profile?.description && ( 84 <p className="text-xs text-zinc-400">{profile.description}</p> 85 )} 86 </div> 87 </div> 88 </div> 89 90 <div className="flex items-center border-b border-zinc-800 mb-8"> 91 <NavLink 92 to={`/profile/${handle}/scrobbles`} 93 className={({ isActive }) => 94 `px-4 py-2 text-xs uppercase tracking-wider ${ 95 isActive ? "text-zinc-100 border-b-2 border-zinc-100" : "text-zinc-500 hover:text-zinc-300" 96 }` 97 } 98 > 99 Scrobbles 100 </NavLink> 101 <NavLink 102 to={`/profile/${handle}/overall`} 103 className={({ isActive }) => 104 `px-4 py-2 text-xs uppercase tracking-wider ${ 105 isActive ? "text-zinc-100 border-b-2 border-zinc-100" : "text-zinc-500 hover:text-zinc-300" 106 }` 107 } 108 > 109 Overall 110 </NavLink> 111 </div> 112 113 {children} 114 </div> 115 </div> 116 ); 117}