/**
* 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 (
);
}