import { useStore } from "@nanostores/react"; import { Suspense, useEffect, useState } from "react"; import { I18nextProvider } from "react-i18next"; import i18n from "../i18n"; import type { UserProfile } from "../types"; declare global { interface Window { __MARGIN_USER__?: UserProfile | null; } } import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate, useParams, } from "react-router-dom"; import { checkSession } from "../api/client"; import MobileNav from "../components/navigation/MobileNav"; import RightSidebar from "../components/navigation/RightSidebar"; import Sidebar from "../components/navigation/Sidebar"; import { $user } from "../store/auth"; import { analytics } from "../lib/analytics"; import AdminModeration from "./core/AdminModeration"; import Discover from "./core/Discover"; import Feed from "./core/Feed"; import New from "./core/New"; import Notifications from "./core/Notifications"; import Search from "./core/Search"; import Settings from "./core/Settings"; import Collections from "./collections/Collections"; import CollectionDetail from "./collections/CollectionDetail"; import AnnotationDetail from "./content/AnnotationDetail"; import UrlPage from "./content/UrlPage"; import UserUrlPage from "./content/UserUrlPage"; import Profile from "./profile/Profile"; const PAGE_TITLES: Record = { "/home": "Home — Margin", "/bookmarks": "Bookmarks — Margin", "/highlights": "Highlights — Margin", "/annotations": "Annotations — Margin", "/discover": "Discover — Margin", "/search": "Search — Margin", "/notifications": "Notifications — Margin", "/new": "New Annotation — Margin", "/settings": "Settings — Margin", "/collections": "Collections — Margin", "/admin/moderation": "Admin — Margin", }; function AuthGuard({ children }: { children: React.ReactNode }) { const user = useStore($user); const [checked, setChecked] = useState(() => "__MARGIN_USER__" in window); useEffect(() => { if (!checked) { const unsub = $user.subscribe(() => setChecked(true)); const t = setTimeout(() => setChecked(true), 3000); return () => { unsub(); clearTimeout(t); }; } }, [checked]); useEffect(() => { if (checked && !user) { window.location.href = "/login"; } }, [checked, user]); if (!checked || !user) return null; return <>{children}; } function CollectionDetailRoute() { const { handle, rkey } = useParams<{ handle: string; rkey: string }>(); return ; } function AnnotationDetailRoute() { const { handle, rkey, type } = useParams<{ handle: string; rkey: string; type: string; }>(); return ; } function AtAnnotationRoute() { const { did, rkey } = useParams<{ did: string; rkey: string }>(); return ; } function AtCollectionAnnotationRoute() { const { did, collection, rkey } = useParams<{ did: string; collection: string; rkey: string; }>(); return ; } function UriAnnotationRoute() { const { uri } = useParams<{ uri: string }>(); return ; } function ProfileRoute() { const { did } = useParams<{ did: string }>(); if (!did) return ; return ; } function UrlRoute() { const params = useParams(); const urlPath = params["*"]; return ; } function UserUrlRoute() { const params = useParams(); return ; } function AppLayout() { const location = useLocation(); const navigate = useNavigate(); const searchParams = new URLSearchParams(location.search); useEffect(() => { document.title = PAGE_TITLES[location.pathname] ?? "Margin"; }, [location.pathname]); useEffect(() => { if (searchParams.get("logged_in") !== "true") return; const user = $user.get(); analytics.capture("login_success", { handle: user?.handle ?? "", pds: undefined, }); const url = new URL(window.location.href); url.searchParams.delete("logged_in"); window.history.replaceState({}, "", url.toString()); // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.search]); useEffect(() => { const SERVER_PATHS = [ "/login", "/about", "/privacy", "/terms", "/brand", "/auth/", "/api/", "/og-image", ]; const handleClick = (e: MouseEvent) => { const a = (e.target as Element).closest("a"); if (!a) return; if (a.hasAttribute("target") || a.hasAttribute("download")) return; const href = a.getAttribute("href"); if (!href || !href.startsWith("/")) return; if (href === "/" || SERVER_PATHS.some((p: string) => href.startsWith(p))) return; e.preventDefault(); navigate(href); }; document.addEventListener("click", handleClick); return () => document.removeEventListener("click", handleClick); }, [navigate]); return (
} /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } />
); } function ProfileSelfRedirect() { const user = useStore($user); if (!user) return null; return ; } export default function AppShell() { useState(() => { const ssrUser = window.__MARGIN_USER__; if (ssrUser !== undefined) { $user.set(ssrUser); } }); useEffect(() => { const ssrUser = window.__MARGIN_USER__; if ($user.get() === null && ssrUser === null) return; if (ssrUser) { checkSession().then((user) => { if (user) $user.set(user); }); } else if (ssrUser === undefined) { checkSession().then((user) => { $user.set(user); }); } }, []); return ( ); }