social components inlay.at
atproto components sdui
86
fork

Configure Feed

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

at main 244 lines 7.1 kB view raw
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}