forked from
chadtmiller.com/slices-teal-relay
Teal.fm frontend powered by slices.network
tealfm-slices.wisp.place
tealfm
slices
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}