Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol. app.exosphere.site
7
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: fix ssr

Hugo 9bcaeb24 16a76e82

+80 -6
+22 -1
packages/app/src/app.tsx
··· 93 93 ); 94 94 } 95 95 96 + function Footer() { 97 + return ( 98 + <footer class={ui.footer}> 99 + <div class={ui.footerInner}> 100 + <span>&copy; {new Date().getFullYear()} Exosphere</span> 101 + <span class={ui.footerSep}>&middot;</span> 102 + <a class={ui.footerLink} href="https://bsky.app/profile/hugo.exosphere.site" target="_blank" rel="noopener noreferrer"> 103 + @hugo.exosphere.site 104 + </a> 105 + <span class={ui.footerSep}>&middot;</span> 106 + <a class={ui.footerLink} href="https://tangled.org/exosphere.site/app" target="_blank" rel="noopener noreferrer"> 107 + Source 108 + </a> 109 + </div> 110 + </footer> 111 + ); 112 + } 113 + 96 114 function Header() { 97 115 const { route } = useLocation(); 98 116 const { loading, authenticated, did, handle } = auth.value; ··· 137 155 <LocationProvider> 138 156 <div class={ui.themeRoot}> 139 157 <Header /> 140 - <MainContent moduleRoutes={moduleRoutes} /> 158 + <div class={ui.mainContent}> 159 + <MainContent moduleRoutes={moduleRoutes} /> 160 + </div> 161 + <Footer /> 141 162 </div> 142 163 </LocationProvider> 143 164 );
-4
packages/app/src/client.tsx
··· 39 39 40 40 hydrate(<App />, document.getElementById("app")!); 41 41 42 - // SSR page data was only needed for hydration — clear it so client-side 43 - // navigations don't reuse stale prefetched data 44 - ssrPageData.value = null; 45 - 46 42 // After hydration, silently refresh data in background to catch changes 47 43 // Use refreshSphere (not loadSphere) to avoid resetting state to pending/null 48 44 if (ssrData) {
+4 -1
packages/client/src/hooks.ts
··· 19 19 const hasInitial = options?.initialData !== undefined; 20 20 const data = useSignal<T | null>(options?.initialData ?? null); 21 21 const pending = useSignal(!hasInitial); 22 - const loading = useSignal(false); 22 + // When there's no initial data, show the loading indicator immediately 23 + // instead of waiting LOADING_DELAY ms. The delay only helps during 24 + // re-fetches where existing data stays visible. 25 + const loading = useSignal(!hasInitial); 23 26 const error = useSignal(""); 24 27 const skipNext = useSignal(hasInitial); 25 28
+41
packages/client/src/ui.css.ts
··· 16 16 17 17 export const themeRoot = style({ 18 18 minHeight: "100vh", 19 + display: "flex", 20 + flexDirection: "column", 19 21 fontFamily: vars.font.body, 20 22 lineHeight: 1.6, 21 23 color: vars.color.text, ··· 585 587 color: vars.color.text, 586 588 boxShadow: `0 1px 2px ${vars.color.shadow}`, 587 589 }); 590 + 591 + // ---- Footer ---- 592 + 593 + export const mainContent = style({ 594 + flex: 1, 595 + }); 596 + 597 + export const footer = style({ 598 + borderBlockStart: `1px solid ${vars.color.border}`, 599 + paddingBlock: vars.space.lg, 600 + marginBlockStart: vars.space.xxl, 601 + transition: "background-color 0.2s, border-color 0.2s", 602 + }); 603 + 604 + export const footerInner = style({ 605 + maxWidth: "640px", 606 + marginInline: "auto", 607 + paddingInline: vars.space.md, 608 + display: "flex", 609 + flexWrap: "wrap", 610 + alignItems: "center", 611 + justifyContent: "center", 612 + gap: vars.space.sm, 613 + fontSize: "0.8125rem", 614 + color: vars.color.textMuted, 615 + }); 616 + 617 + export const footerSep = style({ 618 + color: vars.color.border, 619 + }); 620 + 621 + export const footerLink = style({ 622 + color: vars.color.textMuted, 623 + textDecoration: "none", 624 + transition: "color 0.15s", 625 + ":hover": { 626 + color: vars.color.primary, 627 + }, 628 + });
+8
packages/feature-requests/src/ui/pages/feature-request.tsx
··· 288 288 const prefetchedComments = ssrPageData.peek()?.["feature-request-comments"] as 289 289 | Awaited<ReturnType<typeof getComments>> 290 290 | undefined; 291 + useEffect(() => { 292 + const pd = ssrPageData.peek(); 293 + if (pd && "feature-request-comments" in pd) delete pd["feature-request-comments"]; 294 + }, []); 291 295 292 296 const comments = useSignal<FeatureRequestCommentListItem[]>(prefetchedComments?.comments ?? []); 293 297 const votedCommentIds = useSignal<Set<string>>(new Set()); ··· 485 489 const prefetched = ssrPageData.peek()?.["feature-request"] as 486 490 | Awaited<ReturnType<typeof getFeatureRequest>> 487 491 | undefined; 492 + useEffect(() => { 493 + const pd = ssrPageData.peek(); 494 + if (pd && "feature-request" in pd) delete pd["feature-request"]; 495 + }, []); 488 496 const { data, pending, loading, error, refetch } = useQuery( 489 497 () => getFeatureRequest(number), 490 498 [number],
+5
packages/feature-requests/src/ui/pages/feature-requests.tsx
··· 163 163 const prefetched = ssrPageData.peek()?.[prefetchKey] as 164 164 | Awaited<ReturnType<typeof getFeatureRequests>> 165 165 | undefined; 166 + // Consume SSR key after first render so client-side re-navigation fetches fresh data 167 + useEffect(() => { 168 + const pd = ssrPageData.peek(); 169 + if (pd && prefetchKey in pd) delete pd[prefetchKey]; 170 + }, []); 166 171 const { data, pending, loading, error, refetch } = useQuery( 167 172 () => getFeatureRequests(statuses, sortBy.value, sortOrder.value), 168 173 [activeTab, sortBy.value, sortOrder.value],