import { useQuery } from '@tanstack/react-query';
import { useEffect } from 'react';
import { Notice } from '#/components/atoms/notice.jsx';
import { PublishedCheck } from '#/components/molecules/published-check.jsx';
import { AppSkeleton } from '#/components/organisms/app-skeleton.jsx';
import { Shell } from '#/components/organisms/shell.jsx';
import { get } from '#/lib/api.js';
import { nsidDomain } from '#/lib/format.js';
import { queryKeys } from '#/lib/query.js';
import { useRouter } from '#/lib/router.js';
import { applyTheme } from '#/lib/theme.js';
import { AppPage } from '#/pages/app-page.jsx';
import { AppsPage } from '#/pages/apps.jsx';
import { BackupsPage } from '#/pages/backups.jsx';
import { DrivePage } from '#/pages/drive.jsx';
import { FilesPage } from '#/pages/files.jsx';
import { GitPage } from '#/pages/git.jsx';
import { GitRepoPage } from '#/pages/git-repo-page.jsx';
import { HistoryPage } from '#/pages/history.jsx';
import { HomePage } from '#/pages/home.jsx';
import { IdentityPage } from '#/pages/identity.jsx';
import { PackagesPage } from '#/pages/packages.jsx';
import { RecordsPage } from '#/pages/records.jsx';
import { RegistryPage } from '#/pages/registry.jsx';
import { SecurityPage } from '#/pages/security.jsx';
import { SettingsPage } from '#/pages/settings.jsx';
import { SitesPage } from '#/pages/sites.jsx';
const APPS_PATH = '/account/apps';
const RECORDS_PATH = '/account/repo/records';
const FILES_PATH = '/account/files';
const SITES_PATH = '/account/sites';
const GIT_PATH = '/account/git';
const DRIVE_PATH = '/account/drive';
const REGISTRY_PATH = '/account/registry';
const PACKAGES_PATH = '/account/packages';
/** The account sections, in rail order. Counts come from the overview.
* Sites appears only when the server composes the feature in, under a Labs
* heading at the end of the rail. */
function sectionsFor(data) {
return [
{ path: '/account', title: 'Home' },
{ path: APPS_PATH, title: 'Your apps', count: data.stats.apps },
...(data.features.files
? [{ path: FILES_PATH, title: 'Files', count: data.stats.blobs }]
: []),
{ path: '/account/history', title: 'History' },
{ path: '/account/backups', title: 'Backups' },
{ path: '/account/security', title: 'Sign-in & security' },
{ path: '/account/identity', title: 'Username & host' },
{ path: '/account/settings', title: 'Settings' },
...(data.features.sites ||
data.features.git ||
data.features.drive ||
data.features.oci ||
data.features.npm
? [{ heading: 'Labs' }]
: []),
...(data.features.sites ? [{ path: SITES_PATH, title: 'Sites' }] : []),
...(data.features.git
? [
{
path: GIT_PATH,
title: 'Repositories',
count: data.stats.gitRepos,
},
]
: []),
...(data.features.drive
? [
{
path: DRIVE_PATH,
title: 'Drive',
count: data.stats.driveFiles,
},
]
: []),
...(data.features.oci
? [
{
path: REGISTRY_PATH,
title: 'Registry',
count: data.stats.ociRepos,
},
]
: []),
...(data.features.npm
? [
{
path: PACKAGES_PATH,
title: 'Packages',
count: data.stats.npmPackages,
},
]
: []),
];
}
const PAGES = {
'/account': {
title: 'Your Atmosphere Account',
sub: 'One account for every atproto app. Everything you make is stored here.',
},
[APPS_PATH]: {
title: 'Your apps',
sub: 'Apps that hold access or have stored data. Open one to see its records and manage access.',
},
[FILES_PATH]: {
title: 'Files',
sub: 'Images and other files your apps have stored in your account.',
},
[SITES_PATH]: {
title: 'Sites',
sub: 'Static sites stored in your repo and served on your own domain.',
},
[GIT_PATH]: {
title: 'Repositories',
sub: 'Git repositories stored in your repo. Push with the atproto remote helper, clone from anywhere.',
},
[DRIVE_PATH]: {
title: 'Drive',
sub: 'Files you keep in your own account, in folders you choose.',
},
[REGISTRY_PATH]: {
title: 'Registry',
sub: 'A container registry in your repo. Push with docker, pull from anywhere.',
},
[PACKAGES_PATH]: {
title: 'Packages',
sub: 'An npm registry in your repo. Publish with npm, install from anywhere.',
},
'/account/history': {
title: 'History',
sub: 'Changes recorded by this server, newest first.',
},
'/account/backups': {
title: 'Backups',
sub: 'Scheduled copies of your account, kept somewhere else.',
},
'/account/security': {
title: 'Sign-in & security',
sub: 'How you sign in, and who has access.',
},
'/account/identity': {
title: 'Username & host',
sub: 'Your name on the network, where your account lives, and how to take it elsewhere.',
},
'/account/settings': {
title: 'Settings',
sub: 'How this page looks and behaves.',
},
};
// Clear the account cookie server-side, then hard-navigate to /account, where a
// signed-out request is served the sign-in screen. Used by the sign-out button
// and when a request reveals the current session has lapsed.
function clearSessionAndReload() {
return fetch('/account/sign-out', { method: 'POST', redirect: 'manual' })
.catch(() => {})
.finally(() => {
window.location.href = '/account';
});
}
export function App() {
const { path, params, navigate, back } = useRouter();
const { data, error } = useQuery({
queryKey: queryKeys.overview,
queryFn: () => get('/overview'),
});
const signOut = clearSessionAndReload;
// A 401 means the cookie no longer maps to a live session — it expired, or in
// local dev the account was reseeded under a new DID. Clear the stale cookie
// and reload onto the sign-in screen, rather than leaving the console to error
// on every request.
const sessionExpired = error?.status === 401;
useEffect(() => {
if (sessionExpired) clearSessionAndReload();
}, [sessionExpired]);
// Keep the palette in step with the stored appearance choice, whether it
// arrived with the overview or was changed from another device.
const theme = data?.theme;
useEffect(() => {
if (theme) applyTheme(theme);
}, [theme]);
if (error) {
if (sessionExpired) return