diff --git a/src/components/RecordPreview.tsx b/src/components/RecordPreview.tsx index 729425c..9858d6c 100644 --- a/src/components/RecordPreview.tsx +++ b/src/components/RecordPreview.tsx @@ -18,9 +18,22 @@ type RecordPreviewProps = { collection: string; handle: string; rkey: string; + /** + * When true, suppress the "View Full Record" JSON-modal button and the + * "Open in Explorer" cross-link. Used inside the explorer's record view, + * which already shows the raw (linkified) JSON inline and is itself the + * Explorer destination. + */ + hideExplorerCtas?: boolean; }; -export default function RecordPreview({ record, collection, handle, rkey }: RecordPreviewProps) { +export default function RecordPreview({ + record, + collection, + handle, + rkey, + hideExplorerCtas, +}: RecordPreviewProps) { const { value, cid } = record; const [showJsonModal, setShowJsonModal] = useState(false); @@ -183,39 +196,44 @@ export default function RecordPreview({ record, collection, handle, rkey }: Reco ))} - {/* View Full Record Button */} - + {/* View Full Record Button — hidden when used inside the explorer, + which renders the linkified raw JSON inline immediately below. */} + {!hideExplorerCtas && ( + + )} - {/* Cross-product: jump into the same record inside the explorer */} + {/* Cross-product: jump into the same record inside the explorer — + suppressed when we're already on an explorer page. */} + {!hideExplorerCtas && ( Open in Explorer — backlinks, identity, raw JSON → + )} {/* Footer: CID */} diff --git a/src/components/explore/Breadcrumb.tsx b/src/components/explore/Breadcrumb.tsx index bfc912d..6767633 100644 --- a/src/components/explore/Breadcrumb.tsx +++ b/src/components/explore/Breadcrumb.tsx @@ -4,6 +4,7 @@ import Link from 'next/link'; import { ChevronRight, Server } from 'lucide-react'; import { encodeRepo } from '@/utils/atproto/urls'; import { pdsHostname } from '@/utils/atproto/pdsServer'; +import ShareLinkChip from './ShareLinkChip'; type Props = { handle: string | null; @@ -12,6 +13,11 @@ type Props = { pds?: string; collection?: string; rkey?: string; + /** + * When provided, renders a "Copy link" chip at the end of the breadcrumb + * row. Path or full URL; bare paths get aturi.to prepended. + */ + shareUrl?: string; }; /** @@ -22,7 +28,14 @@ type Props = { * Each upstream segment is clickable. The PDS segment is optional so older * call sites without a resolved PDS still render correctly. */ -export default function Breadcrumb({ handle, did, pds, collection, rkey }: Props) { +export default function Breadcrumb({ + handle, + did, + pds, + collection, + rkey, + shareUrl, +}: Props) { const repoSegment = encodeRepo(handle || did); const repoLabel = handle ? `@${handle}` : did; const pdsHost = pds ? pdsHostname(pds) : null; @@ -112,6 +125,12 @@ export default function Breadcrumb({ handle, did, pds, collection, rkey }: Props {rkey} )} + + {shareUrl && ( + + + + )} ); } diff --git a/src/components/explore/CollectionExplorer.tsx b/src/components/explore/CollectionExplorer.tsx index 25986a2..7502ee2 100644 --- a/src/components/explore/CollectionExplorer.tsx +++ b/src/components/explore/CollectionExplorer.tsx @@ -132,6 +132,8 @@ function CollectionList({ did={identity.did} pds={identity.pds} collection={collection} + // No universal link route for collections — share the explorer URL. + shareUrl={`/explore/${repoSeg}/${collection}`} /> diff --git a/src/components/explore/PdsExplorer.tsx b/src/components/explore/PdsExplorer.tsx index 4018c9e..bbe5734 100644 --- a/src/components/explore/PdsExplorer.tsx +++ b/src/components/explore/PdsExplorer.tsx @@ -14,6 +14,7 @@ import { describeRepo } from '@/utils/atproto/pdsClient'; import { encodeRepo, shortDid } from '@/utils/atproto/urls'; import AppearIn from './AppearIn'; import CopyButton from './CopyButton'; +import ShareLinkChip from './ShareLinkChip'; type Props = { host: string; @@ -190,18 +191,29 @@ function PdsHeader({ >
- - Personal Data Server +
+ + Personal Data Server +
+

diff --git a/src/components/explore/RepoExplorer.tsx b/src/components/explore/RepoExplorer.tsx index 292f864..902202c 100644 --- a/src/components/explore/RepoExplorer.tsx +++ b/src/components/explore/RepoExplorer.tsx @@ -70,7 +70,14 @@ export default function RepoExplorer({ repo }: { repo: string }) { on this view — it's the only \"drill up\" affordance at the top of the explorer hierarchy. */} - + diff --git a/src/components/explore/RichRecordPreview.tsx b/src/components/explore/RichRecordPreview.tsx index d1fd9b7..b056b2c 100644 --- a/src/components/explore/RichRecordPreview.tsx +++ b/src/components/explore/RichRecordPreview.tsx @@ -105,5 +105,13 @@ export default function RichRecordPreview({ return ; } - return ; + return ( + + ); } diff --git a/src/components/explore/ShareLinkChip.tsx b/src/components/explore/ShareLinkChip.tsx new file mode 100644 index 0000000..81ee2de --- /dev/null +++ b/src/components/explore/ShareLinkChip.tsx @@ -0,0 +1,89 @@ +'use client'; + +import { useState } from 'react'; +import { Check, Link2 } from 'lucide-react'; + +type Props = { + /** Path or full URL — `/`-prefixed paths get the production origin prepended. */ + url: string; + /** Override the button text. Defaults to "Copy link". */ + label?: string; + /** Visual hint string for the tooltip / aria-label. Defaults to the resolved URL. */ + title?: string; +}; + +const PRODUCTION_ORIGIN = 'https://aturi.to'; + +async function writeToClipboard(text: string): Promise { + try { + await navigator.clipboard.writeText(text); + return; + } catch { + /* fall through */ + } + const ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.opacity = '0'; + document.body.appendChild(ta); + ta.select(); + try { + document.execCommand('copy'); + } finally { + document.body.removeChild(ta); + } +} + +/** + * Compact "copy this page's link" button used throughout the explorer. + * Accepts a path (gets prefixed with aturi.to) or a full URL. + */ +export default function ShareLinkChip({ url, label = 'Copy link', title }: Props) { + const [copied, setCopied] = useState(false); + + const resolvedUrl = url.startsWith('http') + ? url + : `${PRODUCTION_ORIGIN}${url.startsWith('/') ? url : `/${url}`}`; + + async function onClick() { + await writeToClipboard(resolvedUrl); + setCopied(true); + window.setTimeout(() => setCopied(false), 1500); + } + + return ( + + ); +}