Highly ambitious ATProtocol AppView service and sdks
1import { Link } from "react-router-dom";
2import { Suspense } from "react";
3import { graphql, useLazyLoadQuery } from "react-relay";
4import { Logo } from "./Logo.tsx";
5import { Avatar } from "./Avatar.tsx";
6import { Button } from "./Button.tsx";
7import { logout, useSessionContext } from "../lib/useSession.ts";
8import type { LayoutQuery } from "../__generated__/LayoutQuery.graphql.ts";
9
10interface LayoutProps {
11 children: React.ReactNode;
12 headerChart?: React.ReactNode;
13 subNav?: React.ReactNode;
14}
15
16function UserProfile({ handle }: { handle: string }) {
17 const data = useLazyLoadQuery<LayoutQuery>(
18 graphql`
19 query LayoutQuery($where: NetworkSlicesActorProfileWhereInput) {
20 networkSlicesActorProfiles(first: 1, where: $where) {
21 edges {
22 node {
23 actorHandle
24 avatar {
25 url(preset: "avatar")
26 }
27 }
28 }
29 }
30 }
31 `,
32 {
33 where: {
34 actorHandle: { eq: handle },
35 },
36 },
37 );
38
39 const profile = data.networkSlicesActorProfiles.edges[0]?.node;
40
41 return (
42 <>
43 <Link
44 to={`/profile/${handle}`}
45 className="flex items-center gap-2 px-2 py-1 text-zinc-400 hover:text-zinc-300 transition-colors"
46 >
47 <Avatar
48 src={profile?.avatar?.url}
49 alt={handle}
50 size="sm"
51 />
52 <span>@{handle}</span>
53 </Link>
54 <Link
55 to="/docs"
56 className="px-2 py-1 text-zinc-400 hover:text-zinc-300 transition-colors"
57 >
58 Docs
59 </Link>
60 <Button
61 type="button"
62 onClick={logout}
63 variant="link"
64 size="sm"
65 >
66 Sign Out
67 </Button>
68 </>
69 );
70}
71
72export default function Layout({
73 children,
74 headerChart,
75 subNav,
76}: LayoutProps) {
77 const { session } = useSessionContext();
78 const isAuthenticated = session?.authenticated;
79 const userInfo = session?.user;
80
81 return (
82 <div className="min-h-screen bg-zinc-950 text-zinc-300 font-mono">
83 <div className="max-w-4xl mx-auto px-6 py-12">
84 <div className="border-b border-zinc-800 pb-4 relative">
85 {headerChart && (
86 <div className="absolute inset-0 pointer-events-none opacity-40">
87 {headerChart}
88 </div>
89 )}
90 <div className="flex items-end justify-between relative">
91 <Link
92 to="/"
93 className="flex items-center gap-3 hover:opacity-80 transition-opacity"
94 >
95 <Logo className="w-10 h-10" />
96 <div>
97 <h1 className="text-xs font-medium uppercase tracking-wider text-zinc-500">
98 Slices
99 </h1>
100 <p className="text-xs text-zinc-600 mt-1">network.slices</p>
101 </div>
102 </Link>
103
104 <div className="flex gap-4 text-xs items-center">
105 {isAuthenticated && userInfo
106 ? (
107 <Suspense
108 fallback={
109 <span className="px-2 py-1 text-zinc-400">
110 @{userInfo.handle || userInfo.did}
111 </span>
112 }
113 >
114 <UserProfile handle={userInfo.handle || userInfo.did} />
115 </Suspense>
116 )
117 : (
118 <>
119 <Link
120 to="/waitlist"
121 className="px-2 py-1 text-zinc-500 hover:text-zinc-300 transition-colors"
122 >
123 Join Waitlist
124 </Link>
125 <Link
126 to="/login"
127 className="px-2 py-1 text-zinc-500 hover:text-zinc-300 transition-colors"
128 >
129 Sign In
130 </Link>
131 </>
132 )}
133 </div>
134 </div>
135 </div>
136
137 {subNav && <div className="border-b border-zinc-800">{subNav}</div>}
138
139 <div className="mb-4"></div>
140
141 {children}
142 </div>
143 </div>
144 );
145}