/** * Renders the whole app, its providers around its routes. `main.tsx` mounts * it. Its paths follow one rule (`shared/paths.ts`): `@name` is a user, any * other word under the root a category or an app page, a view of a page one * more segment, and a default view the bare path, with no redirect. * * - `/` is the feed of every dump on the viewer's default tab, and `/hot`, * `/new`, `/journal` and `/follow` its tabs (`Index`). `/?share_url=` opens * the create form on a shared link. * - `/search` and `/search/:searchTab` are the search results (`Search`). * - `/register` and `/login` sign up and sign in (`UserRegister`, * `UserLogin`), signed out only, `/login?next=` coming back to a page. * - `/authorize` lets another gerbeur instance sign the user in with their * account here (`Authorize`), and `/federation/callback` finishes a sign-in * here with an account on another (`FederationCallback`). * - `/reset-password` sets a new password from an emailed link * (`ResetPassword`), and `/unsubscribe` turns off a digest from its emailed * link (`Unsubscribe`). * - `/notifications` (`Notifications`) and `/settings` (`Settings`), signed in * only, and `/manage`, the site's tools, for `category:manage` or * `federation:manage` (`Manage`). * - `/community` and `/community/monthly`, the site's week and month * (`Community`), signed in only. * - `/@:username/*` is a user's (`UserRoutes`): their profile and its tabs, * their dumps at `dumps/:slug` (`Dump`) and `dumps/:slug/edit` (`DumpEdit`, * signed in only), and their playlists at `playlists/:slug` * (`PlaylistDetail`). * - `/:categorySlug` and `/:categorySlug/:feedTab` are a category's feed * (`Index`). * - Any other path is `NotFound`. * * The pages not marked are open to anyone. Every page is loaded with its * route. Since category slugs share the root with the app's pages and the * feed tabs, the server reserves those words as slugs; no slug has an `@`. * * @module */ import { lazy, Suspense } from "react"; import { BrowserRouter, Route, Routes, useLocation, useParams, } from "react-router"; import { RestrictedGuest } from "./pages/RestrictedGuest.tsx"; import { RestrictedLoggedIn } from "./pages/RestrictedLoggedIn.tsx"; import { FEED_TABS, type FeedTab } from "../shared/paths.ts"; import { AuthProvider } from "./contexts/auth/AuthProvider.tsx"; import { PlayerProvider } from "./contexts/player/PlayerProvider.tsx"; import { WSProvider } from "./contexts/ws/WSProvider.tsx"; import { FollowProvider } from "./contexts/social/FollowProvider.tsx"; import { CategoriesProvider } from "./contexts/category/CategoriesProvider.tsx"; import { ThemeProvider } from "./contexts/theme/ThemeProvider.tsx"; import { ChatProvider } from "./contexts/chat/ChatProvider.tsx"; import { GlobalPlayer } from "./components/player/GlobalPlayer.tsx"; import { DumpFab } from "./components/layout/DumpFab.tsx"; import { ChatFab } from "./components/layout/ChatFab.tsx"; import "./App.css"; const Index = lazy(() => import("./pages/Index.tsx").then((m) => ({ default: m.Index })) ); const Dump = lazy(() => import("./pages/Dump.tsx").then((m) => ({ default: m.Dump })) ); const DumpEdit = lazy(() => import("./pages/DumpEdit.tsx").then((m) => ({ default: m.DumpEdit })) ); const UserLogin = lazy(() => import("./pages/UserLogin.tsx").then((m) => ({ default: m.UserLogin })) ); const UserPublicProfile = lazy(() => import("./pages/UserPublicProfile.tsx").then((m) => ({ default: m.UserPublicProfile, })) ); const UserRegister = lazy(() => import("./pages/UserRegister.tsx").then((m) => ({ default: m.UserRegister })) ); const PlaylistDetail = lazy(() => import("./pages/PlaylistDetail.tsx").then((m) => ({ default: m.PlaylistDetail, })) ); const Community = lazy(() => import("./pages/Community.tsx").then((m) => ({ default: m.Community })) ); const Notifications = lazy(() => import("./pages/Notifications.tsx").then((m) => ({ default: m.Notifications, })) ); const Settings = lazy(() => import("./pages/Settings.tsx").then((m) => ({ default: m.Settings })) ); const Manage = lazy(() => import("./pages/Manage.tsx").then((m) => ({ default: m.Manage })) ); const Search = lazy(() => import("./pages/Search.tsx").then((m) => ({ default: m.Search })) ); const ResetPassword = lazy(() => import("./pages/ResetPassword.tsx").then((m) => ({ default: m.ResetPassword, })) ); const Unsubscribe = lazy(() => import("./pages/Unsubscribe.tsx").then((m) => ({ default: m.Unsubscribe })) ); const Authorize = lazy(() => import("./pages/Authorize.tsx").then((m) => ({ default: m.Authorize })) ); const FederationCallback = lazy(() => import("./pages/FederationCallback.tsx").then((m) => ({ default: m.FederationCallback, })) ); const NotFound = lazy(() => import("./pages/NotFound.tsx").then((m) => ({ default: m.NotFound })) ); /** * Renders a feed, of every dump or of `categorySlug`'s, on the tab the path's * segment after the scope names, or the viewer's default one on the bare * path. Every feed path renders it at the same place in the tree, and the key * keeps `Index` mounted on a change of tab, which sorts on the client, and * remounts it on a change of category, so that it refetches. */ function Feed({ categorySlug }: { categorySlug?: string }) { const { pathname } = useLocation(); const segments = pathname.split("/").filter(Boolean); const rest = segments.slice(categorySlug ? 1 : 0); const feedTab = rest[0]; if ( rest.length > 1 || (feedTab && !(FEED_TABS as readonly string[]).includes(feedTab)) ) { return ; } return ( ); } /** * Renders the pages under user `@:handle`, which the page fetches by name: a * former name or another case reaches them too, and the page then puts the * current one in its URL. A tab or view the profile doesn't have is * `NotFound`, which the profile answers itself. */ function UserRoutes() { return ( } /> } /> } /> } /> ); } /** * Dispatches a path's first segment: `@name` is a user's, any other word a * category's feed, the app's own pages having matched before. */ function HandleRoutes() { const { handle = "" } = useParams(); return handle.startsWith("@") ? : ; } /** Renders the route table, and the floating buttons over every page. */ function AppRoutes() { return ( <> } /> {FEED_TABS.map((tab) => ( } /> ))} } /> } /> } /> } /> } /> } /> } /> } /> } /> {["/community", "/community/:view"].map((path) => ( } /> ))} } /> } /> } /> } /> ); } /** * Renders the app. The theme, player, socket, follow and chat providers read * the session, and `ChatProvider` reads the URL, so it sits inside the router. * `GlobalPlayer` sits outside the routes, so playback survives navigation. */ export function App(): React.JSX.Element { return ( ); }