summaryrefslogtreecommitdiff
path: root/apps/web/app/reader
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-08 07:07:59 -0800
committerFuwn <[email protected]>2026-02-08 07:07:59 -0800
commita33dbfa6a1cb1d34ce9a5286efdb25818ff7b6c1 (patch)
tree7d44bdcb94cc1b69fbc201a4757f27f3751c5adb /apps/web/app/reader
parentchore: gate Vercel analytics and speed insights to production only (diff)
downloadasa.news-a33dbfa6a1cb1d34ce9a5286efdb25818ff7b6c1.tar.xz
asa.news-a33dbfa6a1cb1d34ce9a5286efdb25818ff7b6c1.zip
feat: share with highlighted excerpt and fix auth redirect URLs
Add "share" button to text selection toolbar so users can share an entry with a highlighted passage visible to visitors. The public share page renders the highlight and scrolls to it on load. Also fix magic link and password reset redirects to use NEXT_PUBLIC_APP_URL instead of window.location.origin so emails link to the production domain.
Diffstat (limited to 'apps/web/app/reader')
-rw-r--r--apps/web/app/reader/_components/entry-detail-panel.tsx58
-rw-r--r--apps/web/app/reader/_components/highlight-selection-toolbar.tsx9
2 files changed, 67 insertions, 0 deletions
diff --git a/apps/web/app/reader/_components/entry-detail-panel.tsx b/apps/web/app/reader/_components/entry-detail-panel.tsx
index b67ae2a..b772ea1 100644
--- a/apps/web/app/reader/_components/entry-detail-panel.tsx
+++ b/apps/web/app/reader/_components/entry-detail-panel.tsx
@@ -341,6 +341,63 @@ export function EntryDetailPanel({
setSelectionToolbarState(null)
}
+ function handleShareFromSelection() {
+ const container = proseContainerReference.current
+ if (!container || !selectionToolbarState) return
+
+ const serialized = serializeSelectionRange(
+ container,
+ selectionToolbarState.range
+ )
+ if (!serialized) return
+
+ window.getSelection()?.removeAllRanges()
+ setSelectionToolbarState(null)
+ setIsSharePending(true)
+
+ const shareUrlPromise = fetch("/api/share", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ entryIdentifier,
+ highlightedText: serialized.highlightedText,
+ highlightTextOffset: serialized.textOffset,
+ highlightTextLength: serialized.textLength,
+ highlightTextPrefix: serialized.textPrefix,
+ highlightTextSuffix: serialized.textSuffix,
+ }),
+ })
+ .then((response) => {
+ if (!response.ok) throw new Error("failed to create share")
+ return response.json()
+ })
+ .then((data: { shareUrl: string }) => data.shareUrl)
+
+ try {
+ const clipboardItem = new ClipboardItem({
+ "text/plain": shareUrlPromise.then(
+ (url) => new Blob([url], { type: "text/plain" })
+ ),
+ })
+ navigator.clipboard.write([clipboardItem]).then(
+ () => notify("link copied with highlight"),
+ () => shareUrlPromise.then((url) => notify("shared — " + url))
+ )
+ } catch {
+ shareUrlPromise.then((url) => notify("shared — " + url))
+ }
+
+ shareUrlPromise.then(
+ () => {
+ queryClient.invalidateQueries({
+ queryKey: queryKeys.entryShare.single(entryIdentifier),
+ })
+ queryClient.invalidateQueries({ queryKey: ["shared-entries"] })
+ },
+ (error) => notify(error.message)
+ ).finally(() => setIsSharePending(false))
+ }
+
function handleUpdateHighlightNote(note: string | null) {
if (!highlightPopoverState) return
updateHighlightNote.mutate({
@@ -514,6 +571,7 @@ export function EntryDetailPanel({
selectionRect={selectionToolbarState.selectionRect}
containerRect={selectionToolbarState.containerRect}
onHighlight={handleCreateHighlight}
+ onShare={handleShareFromSelection}
onDismiss={() => setSelectionToolbarState(null)}
/>
)}
diff --git a/apps/web/app/reader/_components/highlight-selection-toolbar.tsx b/apps/web/app/reader/_components/highlight-selection-toolbar.tsx
index 42522bf..f7eb8f8 100644
--- a/apps/web/app/reader/_components/highlight-selection-toolbar.tsx
+++ b/apps/web/app/reader/_components/highlight-selection-toolbar.tsx
@@ -6,12 +6,14 @@ interface HighlightSelectionToolbarProperties {
selectionRect: DOMRect
containerRect: DOMRect
onHighlight: (note: string | null) => void
+ onShare: () => void
onDismiss: () => void
}
export function HighlightSelectionToolbar({
selectionRect,
onHighlight,
+ onShare,
onDismiss,
}: HighlightSelectionToolbarProperties) {
const [showNoteInput, setShowNoteInput] = useState(false)
@@ -72,6 +74,13 @@ export function HighlightSelectionToolbar({
>
+ note
</button>
+ <button
+ type="button"
+ onClick={onShare}
+ className="px-2 py-1 text-xs text-text-dim transition-colors hover:bg-background-tertiary hover:text-text-secondary"
+ >
+ share
+ </button>
</div>
)}
</div>