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

fix login modal

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