an app to share curated trails

fix fetch loop

Changed files
+26 -14
app
at
(trail)
+8 -6
app/at/(trail)/[handle]/trail/[rkey]/StopEmbed.tsx
··· 1 1 "use client"; 2 2 3 - import { use } from "react"; 3 + import { use, createContext } from "react"; 4 4 import type { ExternalEmbed } from "@/data/queries"; 5 5 import { LinkPreview } from "./LinkPreview"; 6 6 import { BlueskyPostEmbed } from "./embeds/BlueskyPostEmbed"; ··· 16 16 17 17 type EmbedPromise = Promise<BlueskyPost | LinkMetadata | React.ReactElement | null>; 18 18 export type EmbedCache = Map<string, EmbedPromise>; 19 + export const EmbedCacheContext = createContext<[EmbedCache | null, (c: EmbedCache) => void]>([ 20 + null, 21 + () => {}, 22 + ]); 19 23 20 24 type Props = { 21 25 external: ExternalEmbed; 22 - cache: EmbedCache; 23 26 onDelete?: () => void; 24 27 }; 25 28 ··· 50 53 return promise; 51 54 } 52 55 53 - export function StopEmbed({ external, cache, onDelete }: Props) { 54 - if (typeof window === "undefined") { 55 - throw new Error("Client only"); 56 - } 56 + export function StopEmbed({ external, onDelete }: Props) { 57 + const [cache] = use(EmbedCacheContext); 58 + if (!cache) throw new Error("StopEmbed must be used within EmbedCacheContext.Provider"); 57 59 58 60 const uri = external.uri; 59 61 const isTrail = isTrailUri(uri);
+3 -4
app/at/(trail)/[handle]/trail/[rkey]/TrailStopCard.tsx
··· 1 1 "use client"; 2 2 3 - import { Suspense, useState, useCallback } from "react"; 3 + import { Suspense, use, useCallback } from "react"; 4 4 import { useEditMode } from "./EditModeContext"; 5 5 import type { TrailStop } from "@/data/queries"; 6 - import { StopEmbed, type EmbedCache } from "./StopEmbed"; 6 + import { EmbedCacheContext, StopEmbed } from "./StopEmbed"; 7 7 import { extractLink } from "./utils/linkExtraction"; 8 8 import { getLinkMetadata, blueskyUrlToAtUri } from "./utils/embed-resolver"; 9 9 import "./TrailStopCard.css"; ··· 30 30 const isEditing = !!editContext; 31 31 const updateStop = editContext?.updateStop; 32 32 const error = editContext?.inlineErrors[stop.tid]; 33 - const [embedCache, setEmbedCache] = useState<EmbedCache>(() => new Map()); 33 + const [, setEmbedCache] = use(EmbedCacheContext); 34 34 35 35 const textareaRef = useCallback((el: HTMLTextAreaElement | null) => { 36 36 if (el) { ··· 175 175 <Suspense fallback={<EmbedLoading />}> 176 176 <StopEmbed 177 177 external={stop.external} 178 - cache={embedCache} 179 178 onDelete={isEditing ? handleRemoveLink : undefined} 180 179 /> 181 180 </Suspense>
+15 -4
app/at/(trail)/[handle]/trail/[rkey]/TrailWalk.tsx
··· 1 - import { useState, useLayoutEffect, useRef, Suspense, Activity } from "react"; 1 + import { useState, useLayoutEffect, useRef, Suspense, Activity, type ReactNode } from "react"; 2 2 import { useRouter } from "next/navigation"; 3 3 import type { TrailDetailData } from "@/data/queries"; 4 4 import { BackButton } from "@/app/BackButton"; ··· 10 10 import { TrailWalkersOverlay } from "./TrailWalkersOverlay"; 11 11 import { visitStop, completeTrail, abandonWalk, deleteCompletion } from "@/data/actions"; 12 12 import { useAuthAction } from "@/auth/useAuthAction"; 13 + import type { EmbedCache } from "./StopEmbed"; 13 14 import "./TrailWalk.css"; 15 + 16 + import { EmbedCacheContext } from "./StopEmbed"; 17 + 18 + function RevealedStop({ revealed, children }: { revealed: boolean; children: ReactNode }) { 19 + const cacheAndSetCache = useState<EmbedCache>(() => new Map()); 20 + return ( 21 + <EmbedCacheContext value={cacheAndSetCache}> 22 + <Activity mode={revealed ? "visible" : "hidden"}>{children}</Activity> 23 + </EmbedCacheContext> 24 + ); 25 + } 14 26 15 27 type Props = { 16 28 trail: TrailDetailData; ··· 234 246 const isVisited = 235 247 !isCompleted && index > currentStopIndex && index <= furthestStopIndex; 236 248 const isUnreached = index > furthestStopIndex; 237 - const isHidden = !isCompleted && !isEditMode && isUnreached; 238 249 239 250 const isClickable = 240 251 isEditMode || ··· 244 255 const isReorderActive = isEditMode && isCurrent && !isHoveringStopContent; 245 256 246 257 return ( 247 - <Activity key={stop.tid} mode={isHidden ? "hidden" : "visible"}> 258 + <RevealedStop key={stop.tid} revealed={isCompleted || isEditMode || !isUnreached}> 248 259 <div 249 260 onPointerEnter={() => { 250 261 setIsHoveringStopContent(true); ··· 266 277 onContinue={handleContinue} 267 278 /> 268 279 </div> 269 - </Activity> 280 + </RevealedStop> 270 281 ); 271 282 })} 272 283 </div>