an app to share curated trails sidetrail.app
atproto nextjs react rsc

fix login modal

+26 -35
+2 -3
app/NewTrailButton.tsx
··· 14 14 const router = useRouter(); 15 15 const requireAuth = useAuthAction(); 16 16 17 - const createAction = async () => { 18 - requireAuth(); 17 + const createAction = requireAuth(async () => { 19 18 const rkey = await createDraft(); 20 19 router.push(`/drafts/${rkey}`); 21 - }; 20 + }); 22 21 23 22 return ( 24 23 <ActionButton action={createAction} className="NewTrailButton" pendingChildren="creating...">
+6 -7
app/at/(trail)/[handle]/trail/[rkey]/TrailOverview.tsx
··· 50 50 const lastStop = trail.stops[trail.stops.length - 1]; 51 51 const shouldShowAddButton = lastStop?.title?.trim() && trail.stops.length < 12; 52 52 53 - const startWalkAction = async () => { 54 - requireAuth(); 53 + const startWalkAction = requireAuth(async () => { 55 54 if (!trail.yourWalk && !isEditing) { 56 55 await startWalk(trail.header.uri, trail.header.cid); 57 56 } 58 57 onModeChange("walk"); 59 - }; 58 + }); 60 59 61 - const abandonAction = async () => { 60 + const abandonAction = requireAuth(async () => { 62 61 if (confirm("abandon this trail? your progress will be lost")) { 63 62 if (trail.yourWalk) { 64 63 await abandonWalk(trail.yourWalk!.uri); 65 64 } 66 65 } 67 - }; 66 + }); 68 67 69 - const deleteTrailAction = async () => { 68 + const deleteTrailAction = requireAuth(async () => { 70 69 if (confirm("delete this trail? it will be gone for everyone forever")) { 71 70 await deleteTrail(trail.header.uri); 72 71 router.push("/"); 73 72 } 74 - }; 73 + }); 75 74 76 75 const handleStopTitleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>, stopId: string) => { 77 76 const currentIndex = trail.stops.findIndex((s) => s.tid === stopId);
+6 -7
app/at/(trail)/[handle]/trail/[rkey]/TrailWalk.tsx
··· 163 163 setQueuedScroll({ pending: target }); 164 164 }; 165 165 166 - const handleContinue = async () => { 167 - requireAuth(); 166 + const handleContinue = requireAuth(async () => { 168 167 if (currentStopIndex < stops.length - 1) { 169 168 const nextIndex = currentStopIndex + 1; 170 169 setFurthestStopIndex(Math.max(nextIndex, furthestStopIndex)); ··· 183 182 }, 100); 184 183 } 185 184 } 186 - }; 185 + }); 187 186 188 - const handleGoToStop = async (index: number) => { 187 + const handleGoToStop = requireAuth(async (index: number) => { 189 188 setCurrentStopIndex(index); 190 189 queueScroll(index); 191 190 if (!isEditMode && yourWalk) { 192 191 const stopTid = stops[index].tid; 193 192 await visitStop(yourWalk.uri, stopTid); 194 193 } 195 - }; 194 + }); 196 195 197 196 const handleWriteReflection = () => { 198 197 const trailUrl = `${window.location.origin}/@${header.creator.handle}/trail/${header.rkey}`; ··· 202 201 window.open(`https://bsky.app/intent/compose?text=${text}`, "_blank"); 203 202 }; 204 203 205 - const abandonAction = async () => { 204 + const abandonAction = requireAuth(async () => { 206 205 if (confirm("abandon this trail? your progress will be lost")) { 207 206 if (yourWalk) { 208 207 await abandonWalk(yourWalk.uri); ··· 211 210 }); 212 211 } 213 212 } 214 - }; 213 + }); 215 214 216 215 const isAvatarsActive = !isHoveringStopContent && !isHoveringContainer; 217 216
+2 -3
app/drafts/[rkey]/DraftEditor.tsx
··· 201 201 const [publishError, setPublishError] = useState<string[] | null>(null); 202 202 const [inlineErrors, setInlineErrors] = useState<Record<string, string>>({}); 203 203 204 - const publishAction = async () => { 205 - requireAuth(); 204 + const publishAction = requireAuth(async () => { 206 205 setPublishError(null); 207 206 setInlineErrors({}); 208 207 ··· 229 228 const message = error instanceof Error ? error.message : "something went wrong"; 230 229 setPublishError([message]); 231 230 } 232 - }; 231 + }); 233 232 234 233 const trailDetailData = { 235 234 header: {
-1
app/global-error.tsx
··· 79 79 <h1>lost the trail</h1> 80 80 <p>no worries. reload to get back on track</p> 81 81 <button onClick={() => window.location.reload()}>reload</button> 82 - <p>if this didn't help, try logging out and back in (sorry!)</p> 83 82 </div> 84 83 </body> 85 84 </html>
+9 -13
auth/useAuthAction.ts
··· 3 3 import { useLoginModal } from "@/app/LoginModalContext"; 4 4 import { useAuthContext } from "@/app/AuthContext"; 5 5 6 - export class AuthRequiredError extends Error { 7 - constructor() { 8 - super("Authentication required"); 9 - this.name = "AuthRequiredError"; 10 - } 11 - } 12 - 13 6 export function useAuthAction() { 14 7 const { openLoginModal } = useLoginModal(); 15 8 const { did } = useAuthContext(); 16 9 17 - return () => { 18 - if (!did) { 19 - openLoginModal(); 20 - throw new AuthRequiredError(); 21 - } 22 - }; 10 + return <Args extends unknown[], R>(action: (...args: Args) => Promise<R>) => 11 + async (...args: Args): Promise<R | undefined> => { 12 + if (did) { 13 + return action(...args); 14 + } else { 15 + openLoginModal(); 16 + return undefined; 17 + } 18 + }; 23 19 }
+1 -1
components/ActionButton.tsx
··· 3 3 import { useTransition, type ReactNode } from "react"; 4 4 5 5 type Props = { 6 - action: () => Promise<void> | void; 6 + action: () => Promise<void>; 7 7 children: ReactNode; 8 8 pendingChildren?: ReactNode; 9 9 className?: string;