social components
inlay.at
atproto
components
sdui
1"use client";
2
3import { useState, type ReactNode } from "react";
4import Link from "@/app/link";
5import { useSearchParams, usePathname } from "next/navigation";
6import { useNav, useRecordParams } from "@/app/nav";
7import { parseAtUri } from "@/data/uri";
8import type { FamilyInfo } from "@/data/queries";
9import type { BrowseComponentCard } from "./browse";
10import { BrowseProvider } from "@/app/(render)/client";
11import s from "./browse-shell.module.css";
12import {
13 PreviewCard,
14 PreviewToolbar,
15 ExternalLinkIcon,
16 NavArrow,
17} from "@/app/preview";
18import { PrimaryButton } from "@/app/primary-button";
19import { BrowsePill } from "./browse-pill";
20import { BrowseCard } from "./browse-card";
21import { EmbedButton } from "./embed-button";
22import { DotGrid } from "@/app/dot-grid";
23
24export function BrowseShell({
25 families,
26 cards,
27 authDid,
28 children,
29}: {
30 families: FamilyInfo[];
31 cards: BrowseComponentCard[];
32 authDid: string | null;
33 children: ReactNode;
34}) {
35 const pathname = usePathname();
36 const searchParams = useSearchParams();
37 const { prevHref, nextHref } = useNav();
38 const selectedView =
39 searchParams.get("componentUri") ?? cards[0]?.uri ?? null;
40 const [selectedFamily, setSelectedFamily] = useState<string | null>(null);
41 const { did, collection, rkey } = useRecordParams();
42
43 const filteredCards = selectedFamily
44 ? cards.filter((c) => c.nsid === selectedFamily)
45 : cards;
46
47 function cardHref(card: BrowseComponentCard) {
48 const params = new URLSearchParams(searchParams);
49 params.set("componentUri", card.uri);
50 return `${pathname}?${params.toString()}`;
51 }
52 const sourceUri = `at://${did}/${collection}/${rkey}`;
53
54 function editHref(card: BrowseComponentCard) {
55 const parsed = parseAtUri(card.uri);
56 if (!parsed?.rkey) return pathname;
57 return `/edit/at/${card.authorDid}/${parsed.collection}/${parsed.rkey}?recordUri=${encodeURIComponent(sourceUri)}`;
58 }
59
60 function remixHref(card: BrowseComponentCard) {
61 if (!authDid) return null;
62 return `/edit/at/${authDid}/at.inlay.component/new?recordUri=${encodeURIComponent(sourceUri)}&remixFromUri=${encodeURIComponent(card.uri)}`;
63 }
64
65 const newViewHref = authDid
66 ? `/edit/at/${authDid}/at.inlay.component/new?recordUri=${encodeURIComponent(sourceUri)}`
67 : `/login?returnUrl=${encodeURIComponent(`/edit/at/_/at.inlay.component/new?recordUri=${encodeURIComponent(sourceUri)}`)}`;
68
69 // Expand: open in ephemeral canvas
70 const expandHref = (() => {
71 if (!selectedView) return pathname;
72 const targetDid = authDid ?? parseAtUri(selectedView)?.did;
73 if (!targetDid) return pathname;
74 return `/canvas/at/${targetDid}/at.inlay.canvas/new?componentUri=${encodeURIComponent(selectedView)}&uri=${encodeURIComponent(sourceUri)}`;
75 })();
76
77 const selected = cards.find((c) => c.uri === selectedView);
78
79 return (
80 <>
81 {/* Sidebar */}
82 <div
83 style={{
84 width: 248,
85 minWidth: 248,
86 borderRight: "1px solid var(--border-8)",
87 background: "var(--bg-1)",
88 display: "flex",
89 flexDirection: "column",
90 overflowY: "auto",
91 }}
92 >
93 {/* Family pills */}
94 {families.length > 0 && (
95 <div
96 style={{
97 padding: "10px 12px",
98 display: "flex",
99 flexWrap: "wrap",
100 gap: 5,
101 borderBottom: "1px solid var(--border-8)",
102 }}
103 >
104 <BrowsePill
105 active={selectedFamily === null}
106 count={cards.length}
107 onClick={() => setSelectedFamily(null)}
108 >
109 All
110 </BrowsePill>
111 {families.map((f) => (
112 <BrowsePill
113 key={f.nsid}
114 active={selectedFamily === f.nsid}
115 count={f.count}
116 onClick={() =>
117 setSelectedFamily(selectedFamily === f.nsid ? null : f.nsid)
118 }
119 >
120 {f.displayName}
121 </BrowsePill>
122 ))}
123 </div>
124 )}
125
126 {/* Component list */}
127 <div
128 style={{
129 padding: "6px",
130 display: "flex",
131 flexDirection: "column",
132 gap: 2,
133 flex: 1,
134 }}
135 >
136 {filteredCards.map((card) => (
137 <BrowseCard
138 key={card.uri}
139 href={cardHref(card)}
140 name={card.nsid.split(".").pop()!}
141 subtitle={`by @${card.authorHandle}`}
142 selected={selectedView === card.uri}
143 kind={!card.record.body ? "builtin" : "user"}
144 />
145 ))}
146 </div>
147
148 {/* New component */}
149 <Link href={newViewHref} className={s.newView}>
150 + new component
151 </Link>
152 </div>
153
154 <BrowseContent
155 selected={selected}
156 authDid={authDid}
157 editHref={selected ? editHref(selected) : undefined}
158 remixHref={selected ? remixHref(selected) : undefined}
159 expandHref={expandHref}
160 prevHref={prevHref}
161 nextHref={nextHref}
162 rkey={rkey}
163 recordUri={sourceUri}
164 >
165 <BrowseProvider
166 sourceCollection={collection}
167 sourceRkey={rkey}
168 componentUri={selectedView ?? undefined}
169 >
170 {children}
171 </BrowseProvider>
172 </BrowseContent>
173 </>
174 );
175}
176
177function BrowseContent({
178 selected,
179 authDid,
180 editHref,
181 remixHref,
182 expandHref,
183 prevHref,
184 nextHref,
185 rkey,
186 recordUri,
187 children,
188}: {
189 selected?: BrowseComponentCard;
190 authDid: string | null;
191 editHref?: string;
192 remixHref?: string | null;
193 expandHref: string;
194 prevHref: string | null;
195 nextHref: string | null;
196 rkey: string;
197 recordUri: string;
198 children: ReactNode;
199}) {
200 const isOwn = selected && authDid && selected.authorDid === authDid;
201 const afterLabel = selected ? (
202 <>
203 <EmbedButton componentUri={selected.uri} recordUri={recordUri} />
204 <ExternalLinkIcon href={expandHref} title="Open fullscreen in new tab" />
205 </>
206 ) : undefined;
207
208 const primary = isOwn ? (
209 editHref ? (
210 <PrimaryButton href={editHref}>edit</PrimaryButton>
211 ) : undefined
212 ) : remixHref ? (
213 <PrimaryButton href={remixHref}>remix</PrimaryButton>
214 ) : undefined;
215
216 return (
217 <DotGrid
218 style={{
219 flex: 1,
220 display: "flex",
221 alignItems: "center",
222 justifyContent: "center",
223 minWidth: 0,
224 minHeight: 0,
225 position: "relative",
226 }}
227 >
228 {selected ? (
229 <PreviewToolbar
230 label={selected.nsid.split(".").pop()!}
231 afterLabel={afterLabel}
232 primary={primary}
233 />
234 ) : (
235 <PreviewToolbar />
236 )}
237 <PreviewCard key={selected?.uri} resetKey={`${selected?.uri}:${rkey}`}>
238 {children}
239 </PreviewCard>
240 {prevHref && <NavArrow direction="prev" href={prevHref} />}
241 {nextHref && <NavArrow direction="next" href={nextHref} />}
242 </DotGrid>
243 );
244}