[skip ci] feat: prepare for public release
This commit is contained in:
88
fluxer_app/src/router/components/GuildChannelRouter.tsx
Normal file
88
fluxer_app/src/router/components/GuildChannelRouter.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import {ME} from '~/Constants';
|
||||
import {GuildLayout} from '~/components/layout/GuildLayout';
|
||||
import {useLocation} from '~/lib/router';
|
||||
import {Routes} from '~/Routes';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import SelectedChannelStore from '~/stores/SelectedChannelStore';
|
||||
import {compareChannelPosition, filterViewableChannels} from '~/utils/channelShared';
|
||||
import * as RouterUtils from '~/utils/RouterUtils';
|
||||
|
||||
export const GuildChannelRouter: React.FC<{guildId: string; children?: React.ReactNode}> = observer(
|
||||
({guildId, children}) => {
|
||||
const location = useLocation();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (guildId === ME || location.pathname === Routes.ME) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (MobileLayoutStore.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname.startsWith('/channels/') && !location.pathname.startsWith(Routes.ME)) {
|
||||
if (location.pathname.split('/').length === 3) {
|
||||
const pathSegments = location.pathname.split('/');
|
||||
const currentGuildId = pathSegments[2];
|
||||
|
||||
if (currentGuildId !== guildId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedChannelId = SelectedChannelStore.selectedChannelIds.get(guildId);
|
||||
|
||||
if (selectedChannelId) {
|
||||
const channel = ChannelStore.getChannel(selectedChannelId);
|
||||
if (channel && channel.guildId === guildId) {
|
||||
RouterUtils.replaceWith(Routes.guildChannel(guildId, selectedChannelId));
|
||||
} else {
|
||||
const channels = ChannelStore.getGuildChannels(guildId);
|
||||
const viewableChannels = filterViewableChannels(channels).sort(compareChannelPosition);
|
||||
|
||||
if (viewableChannels.length > 0) {
|
||||
const firstChannel = viewableChannels[0];
|
||||
RouterUtils.replaceWith(Routes.guildChannel(guildId, firstChannel.id));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const channels = ChannelStore.getGuildChannels(guildId);
|
||||
const viewableChannels = filterViewableChannels(channels).sort(compareChannelPosition);
|
||||
|
||||
if (viewableChannels.length > 0) {
|
||||
const firstChannel = viewableChannels[0];
|
||||
RouterUtils.replaceWith(Routes.guildChannel(guildId, firstChannel.id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [guildId, location.pathname, MobileLayoutStore.enabled]);
|
||||
|
||||
if (guildId === ME || location.pathname === Routes.ME) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <GuildLayout>{children}</GuildLayout>;
|
||||
},
|
||||
);
|
||||
331
fluxer_app/src/router/components/RootComponent.tsx
Normal file
331
fluxer_app/src/router/components/RootComponent.tsx
Normal file
@@ -0,0 +1,331 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import * as AuthenticationActionCreators from '~/actions/AuthenticationActionCreators';
|
||||
import {KeyboardModeListener} from '~/components/layout/KeyboardModeListener';
|
||||
import {MobileBottomNav} from '~/components/layout/MobileBottomNav';
|
||||
import {SplashScreen} from '~/components/layout/SplashScreen';
|
||||
import {useLocation} from '~/lib/router';
|
||||
import SessionManager from '~/lib/SessionManager';
|
||||
import {Routes} from '~/Routes';
|
||||
import {isAutoRedirectExemptPath} from '~/router/constants';
|
||||
import * as PushSubscriptionService from '~/services/push/PushSubscriptionService';
|
||||
import AccountManager from '~/stores/AccountManager';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import ConnectionStore from '~/stores/ConnectionStore';
|
||||
import InitializationStore from '~/stores/InitializationStore';
|
||||
import LocationStore from '~/stores/LocationStore';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import UserStore from '~/stores/UserStore';
|
||||
import {navigateToWithMobileHistory} from '~/utils/MobileNavigation';
|
||||
import {isInstalledPwa} from '~/utils/PwaUtils';
|
||||
import * as RouterUtils from '~/utils/RouterUtils';
|
||||
|
||||
const RootComponent: React.FC<{children?: React.ReactNode}> = observer(({children}) => {
|
||||
const location = useLocation();
|
||||
const isAuthenticated = AuthenticationStore.isAuthenticated;
|
||||
const mobileLayoutState = MobileLayoutStore;
|
||||
const [hasRestoredLocation, setHasRestoredLocation] = React.useState(false);
|
||||
const currentUser = UserStore.currentUser;
|
||||
const [hasHandledNotificationNav, setHasHandledNotificationNav] = React.useState(false);
|
||||
const [previousMobileLayoutState, setPreviousMobileLayoutState] = React.useState(mobileLayoutState.enabled);
|
||||
const lastMobileHistoryBuildRef = React.useRef<{ts: number; path: string} | null>(null);
|
||||
const isLocationStoreHydrated = LocationStore.isHydrated;
|
||||
const canNavigateToProtectedRoutes = InitializationStore.canNavigateToProtectedRoutes;
|
||||
const pendingRedirectRef = React.useRef<string | null>(null);
|
||||
|
||||
const hasStartedRestoreRef = React.useRef(false);
|
||||
const pathname = location.pathname;
|
||||
const isDesktopHandoff = location.searchParams.get('desktop_handoff') === '1';
|
||||
const isAutoRedirectExemptRoute = isAutoRedirectExemptPath(pathname);
|
||||
const shouldSkipAutoRedirect = isAutoRedirectExemptRoute || (pathname === Routes.LOGIN && isDesktopHandoff);
|
||||
|
||||
const isAuthRoute = React.useMemo(() => {
|
||||
return (
|
||||
pathname.startsWith(Routes.LOGIN) ||
|
||||
pathname.startsWith(Routes.REGISTER) ||
|
||||
pathname.startsWith(Routes.FORGOT_PASSWORD) ||
|
||||
pathname.startsWith(Routes.RESET_PASSWORD) ||
|
||||
pathname.startsWith(Routes.VERIFY_EMAIL) ||
|
||||
pathname.startsWith(Routes.AUTHORIZE_IP) ||
|
||||
pathname.startsWith(Routes.EMAIL_REVERT) ||
|
||||
pathname.startsWith(Routes.OAUTH_AUTHORIZE) ||
|
||||
pathname.startsWith(Routes.REPORT) ||
|
||||
pathname.startsWith('/invite/') ||
|
||||
pathname.startsWith('/gift/') ||
|
||||
pathname.startsWith('/theme/')
|
||||
);
|
||||
}, [pathname]);
|
||||
|
||||
const shouldBypassGateway = isAuthRoute && pathname !== Routes.PENDING_VERIFICATION;
|
||||
const authToken = AuthenticationStore.authToken;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!SessionManager.isInitialized) return;
|
||||
if (AccountManager.isSwitching) return;
|
||||
|
||||
const isAuth = AuthenticationStore.isAuthenticated;
|
||||
|
||||
if (isAuth && isAuthRoute) return;
|
||||
|
||||
if (shouldBypassGateway) {
|
||||
if (isAuth) {
|
||||
if (!shouldSkipAutoRedirect) {
|
||||
RouterUtils.replaceWith(Routes.ME);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (ConnectionStore.isConnected || ConnectionStore.isConnecting || ConnectionStore.socket) {
|
||||
ConnectionStore.logout();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isAuth) {
|
||||
const current = pathname + window.location.search;
|
||||
if (!pendingRedirectRef.current) {
|
||||
pendingRedirectRef.current = current;
|
||||
}
|
||||
RouterUtils.replaceWith(`${Routes.LOGIN}?redirect_to=${encodeURIComponent(pendingRedirectRef.current)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isAuth && InitializationStore.isLoading) {
|
||||
void AuthenticationActionCreators.ensureSessionStarted();
|
||||
}
|
||||
}, [
|
||||
SessionManager.isInitialized,
|
||||
authToken,
|
||||
AccountManager.isSwitching,
|
||||
AuthenticationStore.isAuthenticated,
|
||||
ConnectionStore.isConnected,
|
||||
ConnectionStore.isConnecting,
|
||||
InitializationStore.isLoading,
|
||||
shouldBypassGateway,
|
||||
shouldSkipAutoRedirect,
|
||||
pendingRedirectRef,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!AuthenticationStore.isAuthenticated) return;
|
||||
const target = pendingRedirectRef.current;
|
||||
if (!target) return;
|
||||
|
||||
const current = location.pathname + window.location.search;
|
||||
if (current !== target) {
|
||||
RouterUtils.replaceWith(target);
|
||||
}
|
||||
|
||||
pendingRedirectRef.current = null;
|
||||
}, [AuthenticationStore.isAuthenticated, location.pathname]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!isAuthenticated ||
|
||||
hasRestoredLocation ||
|
||||
hasStartedRestoreRef.current ||
|
||||
!canNavigateToProtectedRoutes ||
|
||||
!isLocationStoreHydrated
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (location.pathname === Routes.HOME) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasStartedRestoreRef.current = true;
|
||||
setHasRestoredLocation(true);
|
||||
|
||||
const lastLocation = LocationStore.getLastLocation();
|
||||
if (lastLocation && lastLocation !== location.pathname && location.pathname === Routes.ME) {
|
||||
navigateToWithMobileHistory(lastLocation, mobileLayoutState.enabled);
|
||||
} else if (mobileLayoutState.enabled) {
|
||||
const p = location.pathname;
|
||||
if ((Routes.isDMRoute(p) && p !== Routes.ME) || (Routes.isGuildChannelRoute(p) && p.split('/').length === 4)) {
|
||||
navigateToWithMobileHistory(p, true);
|
||||
setHasHandledNotificationNav(true);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
isAuthenticated,
|
||||
hasRestoredLocation,
|
||||
mobileLayoutState.enabled,
|
||||
isLocationStoreHydrated,
|
||||
canNavigateToProtectedRoutes,
|
||||
location.pathname,
|
||||
]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isAuthenticated || !hasRestoredLocation) return;
|
||||
|
||||
if (previousMobileLayoutState !== mobileLayoutState.enabled) {
|
||||
setPreviousMobileLayoutState(mobileLayoutState.enabled);
|
||||
|
||||
if (mobileLayoutState.enabled) {
|
||||
const currentPath = location.pathname;
|
||||
if (
|
||||
(Routes.isDMRoute(currentPath) && currentPath !== Routes.ME) ||
|
||||
(Routes.isGuildChannelRoute(currentPath) && currentPath.split('/').length === 4)
|
||||
) {
|
||||
navigateToWithMobileHistory(currentPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated, hasRestoredLocation, mobileLayoutState.enabled, previousMobileLayoutState, location.pathname]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const shouldSaveLocation = Routes.isChannelRoute(location.pathname) || Routes.isSpecialPage(location.pathname);
|
||||
|
||||
if (isAuthenticated && shouldSaveLocation) {
|
||||
LocationStore.saveLocation(location.pathname);
|
||||
}
|
||||
}, [isAuthenticated, location.pathname]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isAuthenticated || !hasRestoredLocation) return;
|
||||
|
||||
if (previousMobileLayoutState !== mobileLayoutState.enabled) {
|
||||
setPreviousMobileLayoutState(mobileLayoutState.enabled);
|
||||
|
||||
if (mobileLayoutState.enabled) {
|
||||
const currentPath = location.pathname;
|
||||
|
||||
const now = Date.now();
|
||||
const last = lastMobileHistoryBuildRef.current;
|
||||
if (last && last.path === currentPath && now - last.ts < 1500) {
|
||||
return;
|
||||
}
|
||||
lastMobileHistoryBuildRef.current = {ts: now, path: currentPath};
|
||||
if (
|
||||
(Routes.isDMRoute(currentPath) && currentPath !== Routes.ME) ||
|
||||
(Routes.isGuildChannelRoute(currentPath) && currentPath.split('/').length === 4)
|
||||
) {
|
||||
if (Routes.isDMRoute(currentPath) && currentPath !== Routes.ME) {
|
||||
RouterUtils.replaceWith(Routes.ME);
|
||||
setTimeout(() => RouterUtils.transitionTo(currentPath), 0);
|
||||
} else if (Routes.isGuildChannelRoute(currentPath) && currentPath.split('/').length === 4) {
|
||||
const parts = currentPath.split('/');
|
||||
const guildId = parts[2];
|
||||
const guildPath = Routes.guildChannel(guildId);
|
||||
RouterUtils.replaceWith(guildPath);
|
||||
setTimeout(() => RouterUtils.transitionTo(currentPath), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [isAuthenticated, hasRestoredLocation, mobileLayoutState.enabled, previousMobileLayoutState, location.pathname]);
|
||||
|
||||
const navigateWithHistoryStack = React.useCallback(
|
||||
(url: string) => {
|
||||
navigateToWithMobileHistory(url, mobileLayoutState.enabled);
|
||||
},
|
||||
[mobileLayoutState.enabled],
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isAuthenticated || !mobileLayoutState.enabled) return;
|
||||
|
||||
const handleNotificationNavigate = (event: MessageEvent) => {
|
||||
if (event.data?.type === 'NOTIFICATION_CLICK_NAVIGATE') {
|
||||
if (hasHandledNotificationNav) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = event.data.url;
|
||||
const targetUserId = event.data.targetUserId as string | undefined;
|
||||
|
||||
void (async () => {
|
||||
if (targetUserId && targetUserId !== AccountManager.currentUserId && AccountManager.canSwitchAccounts) {
|
||||
try {
|
||||
await AccountManager.switchToAccount(targetUserId);
|
||||
} catch (error) {
|
||||
console.error('Failed to switch account for notification', error);
|
||||
}
|
||||
}
|
||||
|
||||
navigateWithHistoryStack(url);
|
||||
setHasHandledNotificationNav(true);
|
||||
})();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.data?.type === 'PUSH_SUBSCRIPTION_CHANGE') {
|
||||
if (isInstalledPwa()) {
|
||||
void PushSubscriptionService.registerPushSubscription();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!hasHandledNotificationNav) {
|
||||
const urlParams = location.searchParams;
|
||||
if (urlParams.get('fromNotification') === '1') {
|
||||
const newParams = new URLSearchParams(urlParams);
|
||||
newParams.delete('fromNotification');
|
||||
const cleanPath = location.pathname + (newParams.toString() ? `?${newParams.toString()}` : '');
|
||||
|
||||
navigateWithHistoryStack(cleanPath);
|
||||
setHasHandledNotificationNav(true);
|
||||
}
|
||||
}
|
||||
|
||||
navigator.serviceWorker?.addEventListener('message', handleNotificationNavigate);
|
||||
|
||||
return () => {
|
||||
navigator.serviceWorker?.removeEventListener('message', handleNotificationNavigate);
|
||||
};
|
||||
}, [isAuthenticated, mobileLayoutState.enabled, hasHandledNotificationNav, location, navigateWithHistoryStack]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (currentUser?.pendingManualVerification) {
|
||||
if (
|
||||
pathname !== Routes.PENDING_VERIFICATION &&
|
||||
!pathname.startsWith('/login') &&
|
||||
!pathname.startsWith('/register')
|
||||
) {
|
||||
RouterUtils.replaceWith(Routes.PENDING_VERIFICATION);
|
||||
}
|
||||
}
|
||||
}, [currentUser, pathname]);
|
||||
|
||||
const showBottomNav =
|
||||
mobileLayoutState.enabled &&
|
||||
(location.pathname === Routes.ME ||
|
||||
Routes.isFavoritesRoute(location.pathname) ||
|
||||
location.pathname === Routes.NOTIFICATIONS ||
|
||||
location.pathname === Routes.YOU ||
|
||||
(Routes.isGuildChannelRoute(location.pathname) && location.pathname.split('/').length === 3));
|
||||
|
||||
if (isAuthenticated && !canNavigateToProtectedRoutes && !shouldBypassGateway) {
|
||||
return <SplashScreen />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<KeyboardModeListener />
|
||||
{children}
|
||||
{showBottomNav && currentUser && <MobileBottomNav currentUser={currentUser} />}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export {RootComponent};
|
||||
39
fluxer_app/src/router/constants.ts
Normal file
39
fluxer_app/src/router/constants.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {Routes} from '~/Routes';
|
||||
|
||||
export const AUTO_REDIRECT_EXEMPT_PATHS = new Set<string>([
|
||||
Routes.RESET_PASSWORD,
|
||||
Routes.AUTHORIZE_IP,
|
||||
Routes.EMAIL_REVERT,
|
||||
Routes.VERIFY_EMAIL,
|
||||
Routes.OAUTH_AUTHORIZE,
|
||||
Routes.REPORT,
|
||||
]);
|
||||
|
||||
const AUTO_REDIRECT_EXEMPT_PREFIXES = ['/invite/', '/gift/', '/theme/', '/oauth2/'];
|
||||
|
||||
export const isAutoRedirectExemptPath = (pathname: string): boolean => {
|
||||
if (AUTO_REDIRECT_EXEMPT_PATHS.has(pathname)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return AUTO_REDIRECT_EXEMPT_PREFIXES.some((prefix) => pathname.startsWith(prefix));
|
||||
};
|
||||
225
fluxer_app/src/router/routes/appRoutes.tsx
Normal file
225
fluxer_app/src/router/routes/appRoutes.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import React from 'react';
|
||||
import {ChannelTypes, ME} from '~/Constants';
|
||||
import {AppBadge} from '~/components/AppBadge';
|
||||
import {ChannelIndexPage} from '~/components/channel/ChannelIndexPage';
|
||||
import {ChannelLayout} from '~/components/channel/ChannelLayout';
|
||||
import {DMLayout} from '~/components/channel/dm/DMLayout';
|
||||
import {AppLayout} from '~/components/layout/AppLayout';
|
||||
import {FavoritesLayout} from '~/components/layout/FavoritesLayout';
|
||||
import {GuildsLayout} from '~/components/layout/GuildsLayout';
|
||||
import {BookmarksBottomSheet} from '~/components/modals/BookmarksBottomSheet';
|
||||
import {StatusChangeBottomSheet} from '~/components/modals/StatusChangeBottomSheet';
|
||||
import {NotificationsPage} from '~/components/pages/NotificationsPage';
|
||||
import PremiumCallbackPage from '~/components/pages/PremiumCallbackPage';
|
||||
import {YouPage} from '~/components/pages/YouPage';
|
||||
import {createRoute, Redirect, useParams} from '~/lib/router';
|
||||
import SessionManager from '~/lib/SessionManager';
|
||||
import {Routes} from '~/Routes';
|
||||
import {GuildChannelRouter} from '~/router/components/GuildChannelRouter';
|
||||
import {rootRoute} from '~/router/routes/rootRoutes';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import ChannelStore from '~/stores/ChannelStore';
|
||||
import MobileLayoutStore from '~/stores/MobileLayoutStore';
|
||||
import SelectedChannelStore from '~/stores/SelectedChannelStore';
|
||||
|
||||
const appLayoutRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
id: 'appLayout',
|
||||
onEnter: () => {
|
||||
if (!SessionManager.isInitialized) {
|
||||
return undefined;
|
||||
}
|
||||
if (!AuthenticationStore.isAuthenticated) {
|
||||
const current = window.location.pathname + window.location.search;
|
||||
return new Redirect(`${Routes.LOGIN}?redirect_to=${encodeURIComponent(current)}`);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
layout: ({children}) => (
|
||||
<>
|
||||
<AppBadge />
|
||||
<AppLayout>{children}</AppLayout>
|
||||
</>
|
||||
),
|
||||
});
|
||||
|
||||
const guildsLayoutRoute = createRoute({
|
||||
getParentRoute: () => appLayoutRoute,
|
||||
id: 'guildsLayout',
|
||||
layout: ({children}) => <GuildsLayout>{children}</GuildsLayout>,
|
||||
});
|
||||
|
||||
const notificationsRoute = createRoute({
|
||||
getParentRoute: () => appLayoutRoute,
|
||||
id: 'notifications',
|
||||
path: Routes.NOTIFICATIONS,
|
||||
component: () => {
|
||||
const [bookmarksSheetOpen, setBookmarksSheetOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NotificationsPage onBookmarksClick={() => setBookmarksSheetOpen(true)} />
|
||||
<BookmarksBottomSheet isOpen={bookmarksSheetOpen} onClose={() => setBookmarksSheetOpen(false)} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const youRoute = createRoute({
|
||||
getParentRoute: () => appLayoutRoute,
|
||||
id: 'you',
|
||||
path: Routes.YOU,
|
||||
component: () => {
|
||||
const [statusSheetOpen, setStatusSheetOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<YouPage onAvatarClick={() => setStatusSheetOpen(true)} />
|
||||
<StatusChangeBottomSheet isOpen={statusSheetOpen} onClose={() => setStatusSheetOpen(false)} />
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const premiumCallbackRoute = createRoute({
|
||||
getParentRoute: () => appLayoutRoute,
|
||||
id: 'premiumCallback',
|
||||
path: Routes.PREMIUM_CALLBACK,
|
||||
component: () => <PremiumCallbackPage />,
|
||||
});
|
||||
|
||||
const bookmarksRoute = createRoute({
|
||||
getParentRoute: () => guildsLayoutRoute,
|
||||
id: 'bookmarks',
|
||||
path: Routes.BOOKMARKS,
|
||||
component: () => <DMLayout />,
|
||||
});
|
||||
|
||||
const mentionsRoute = createRoute({
|
||||
getParentRoute: () => guildsLayoutRoute,
|
||||
id: 'mentions',
|
||||
path: Routes.MENTIONS,
|
||||
component: () => <DMLayout />,
|
||||
});
|
||||
|
||||
const meRoute = createRoute({
|
||||
getParentRoute: () => guildsLayoutRoute,
|
||||
id: 'me',
|
||||
path: '/channels/@me',
|
||||
component: observer(() => {
|
||||
const isMobileLayout = MobileLayoutStore.enabled;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isMobileLayout && SelectedChannelStore.selectedChannelIds.has(ME)) {
|
||||
SelectedChannelStore.clearGuildSelection(ME);
|
||||
}
|
||||
}, [isMobileLayout]);
|
||||
|
||||
return <DMLayout />;
|
||||
}),
|
||||
});
|
||||
|
||||
const favoritesRoute = createRoute({
|
||||
getParentRoute: () => guildsLayoutRoute,
|
||||
id: 'favorites',
|
||||
path: '/channels/@favorites',
|
||||
layout: ({children}) => <FavoritesLayout>{children}</FavoritesLayout>,
|
||||
});
|
||||
|
||||
const favoritesChannelRoute = createRoute({
|
||||
getParentRoute: () => favoritesRoute,
|
||||
id: 'favoritesChannel',
|
||||
path: '/channels/@favorites/:channelId',
|
||||
component: () => (
|
||||
<ChannelLayout>
|
||||
<ChannelIndexPage />
|
||||
</ChannelLayout>
|
||||
),
|
||||
});
|
||||
|
||||
const channelsRoute = createRoute({
|
||||
getParentRoute: () => guildsLayoutRoute,
|
||||
id: 'channels',
|
||||
path: '/channels/:guildId',
|
||||
layout: ({children}) => {
|
||||
const params = useParams() as {guildId: string};
|
||||
const {guildId} = params;
|
||||
|
||||
if (guildId === ME) {
|
||||
return <DMLayout>{children}</DMLayout>;
|
||||
}
|
||||
|
||||
return guildId ? <GuildChannelRouter guildId={guildId}>{children}</GuildChannelRouter> : null;
|
||||
},
|
||||
});
|
||||
|
||||
const channelRoute = createRoute({
|
||||
getParentRoute: () => channelsRoute,
|
||||
id: 'channel',
|
||||
path: '/channels/:guildId/:channelId',
|
||||
onEnter: (ctx) => {
|
||||
const {guildId, channelId} = ctx.params;
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (channel && (channel.type === ChannelTypes.GUILD_CATEGORY || channel.type === ChannelTypes.GUILD_LINK)) {
|
||||
return new Redirect(Routes.guildChannel(guildId));
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
component: () => (
|
||||
<ChannelLayout>
|
||||
<ChannelIndexPage />
|
||||
</ChannelLayout>
|
||||
),
|
||||
});
|
||||
|
||||
const messageRoute = createRoute({
|
||||
getParentRoute: () => channelRoute,
|
||||
id: 'message',
|
||||
path: '/channels/:guildId/:channelId/:messageId',
|
||||
onEnter: (ctx) => {
|
||||
const {guildId, channelId} = ctx.params;
|
||||
const channel = ChannelStore.getChannel(channelId);
|
||||
if (channel && (channel.type === ChannelTypes.GUILD_CATEGORY || channel.type === ChannelTypes.GUILD_LINK)) {
|
||||
return new Redirect(Routes.guildChannel(guildId));
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
component: () => (
|
||||
<ChannelLayout>
|
||||
<ChannelIndexPage />
|
||||
</ChannelLayout>
|
||||
),
|
||||
});
|
||||
|
||||
export const appRouteTree = appLayoutRoute.addChildren([
|
||||
notificationsRoute,
|
||||
youRoute,
|
||||
premiumCallbackRoute,
|
||||
guildsLayoutRoute.addChildren([
|
||||
bookmarksRoute,
|
||||
mentionsRoute,
|
||||
meRoute,
|
||||
favoritesRoute.addChildren([favoritesChannelRoute]),
|
||||
channelsRoute.addChildren([channelRoute.addChildren([messageRoute])]),
|
||||
]),
|
||||
]);
|
||||
310
fluxer_app/src/router/routes/authRoutes.tsx
Normal file
310
fluxer_app/src/router/routes/authRoutes.tsx
Normal file
@@ -0,0 +1,310 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {i18n} from '@lingui/core';
|
||||
import * as GiftActionCreators from '~/actions/GiftActionCreators';
|
||||
import * as InviteActionCreators from '~/actions/InviteActionCreators';
|
||||
import * as ThemeActionCreators from '~/actions/ThemeActionCreators';
|
||||
import {AuthLayout} from '~/components/layout/AuthLayout';
|
||||
import AuthorizeIPPage from '~/components/pages/AuthorizeIPPage';
|
||||
import EmailRevertPage from '~/components/pages/EmailRevertPage';
|
||||
import ForgotPasswordPage from '~/components/pages/ForgotPasswordPage';
|
||||
import GiftLoginPage from '~/components/pages/GiftLoginPage';
|
||||
import GiftRegisterPage from '~/components/pages/GiftRegisterPage';
|
||||
import InviteLoginPage from '~/components/pages/InviteLoginPage';
|
||||
import InviteRegisterPage from '~/components/pages/InviteRegisterPage';
|
||||
import LoginPage from '~/components/pages/LoginPage';
|
||||
import OAuthAuthorizePage from '~/components/pages/OAuthAuthorizePage';
|
||||
import PendingVerificationPage from '~/components/pages/PendingVerificationPage';
|
||||
import RegisterPage from '~/components/pages/RegisterPage';
|
||||
import {ReportPage} from '~/components/pages/ReportPage';
|
||||
import ResetPasswordPage from '~/components/pages/ResetPasswordPage';
|
||||
import ThemeLoginPage from '~/components/pages/ThemeLoginPage';
|
||||
import ThemeRegisterPage from '~/components/pages/ThemeRegisterPage';
|
||||
import VerifyEmailPage from '~/components/pages/VerifyEmailPage';
|
||||
import {createRoute, Redirect, type RouteContext} from '~/lib/router';
|
||||
import SessionManager from '~/lib/SessionManager';
|
||||
import {Routes} from '~/Routes';
|
||||
import {rootRoute} from '~/router/routes/rootRoutes';
|
||||
import AuthenticationStore from '~/stores/AuthenticationStore';
|
||||
import RuntimeConfigStore from '~/stores/RuntimeConfigStore';
|
||||
import * as RouterUtils from '~/utils/RouterUtils';
|
||||
|
||||
const resolveToPath = (to: Redirect['to']): string => {
|
||||
if (typeof to === 'string') {
|
||||
return to;
|
||||
}
|
||||
|
||||
const url = new URL(to.to, window.location.origin);
|
||||
|
||||
if (to.search) {
|
||||
const sp = new URLSearchParams();
|
||||
for (const [k, v] of Object.entries(to.search)) {
|
||||
if (v === undefined) continue;
|
||||
if (v === null) {
|
||||
sp.set(k, '');
|
||||
} else {
|
||||
sp.set(k, String(v));
|
||||
}
|
||||
}
|
||||
url.search = sp.toString() ? `?${sp.toString()}` : '';
|
||||
}
|
||||
|
||||
if (to.hash) {
|
||||
url.hash = to.hash.startsWith('#') ? to.hash : `#${to.hash}`;
|
||||
}
|
||||
|
||||
return url.pathname + url.search + url.hash;
|
||||
};
|
||||
|
||||
type AuthRedirectHandler = (ctx: RouteContext) => Redirect | undefined;
|
||||
|
||||
const whenAuthenticated = (handler: AuthRedirectHandler) => {
|
||||
return (ctx: RouteContext): Redirect | undefined => {
|
||||
const execute = (): Redirect | undefined => handler(ctx);
|
||||
|
||||
if (SessionManager.isInitialized) {
|
||||
return AuthenticationStore.isAuthenticated ? execute() : undefined;
|
||||
}
|
||||
|
||||
void SessionManager.initialize().then(() => {
|
||||
if (AuthenticationStore.isAuthenticated) {
|
||||
const res = execute();
|
||||
if (res instanceof Redirect) {
|
||||
RouterUtils.replaceWith(resolveToPath(res.to));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return undefined;
|
||||
};
|
||||
};
|
||||
|
||||
const authLayoutRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
id: 'authLayout',
|
||||
layout: ({children}) => <AuthLayout>{children}</AuthLayout>,
|
||||
});
|
||||
|
||||
const loginRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'login',
|
||||
path: '/login',
|
||||
onEnter: whenAuthenticated(() => {
|
||||
const search = window.location.search;
|
||||
const qp = new URLSearchParams(search);
|
||||
const isDesktopHandoff = qp.get('desktop_handoff') === '1';
|
||||
if (isDesktopHandoff) {
|
||||
return undefined;
|
||||
}
|
||||
const redirectTo = qp.get('redirect_to');
|
||||
return new Redirect(redirectTo || Routes.ME);
|
||||
}),
|
||||
component: () => <LoginPage />,
|
||||
});
|
||||
|
||||
const registerRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'register',
|
||||
path: '/register',
|
||||
onEnter: whenAuthenticated(() => {
|
||||
const search = window.location.search;
|
||||
const qp = new URLSearchParams(search);
|
||||
const redirectTo = qp.get('redirect_to');
|
||||
return new Redirect(redirectTo || Routes.ME);
|
||||
}),
|
||||
component: () => <RegisterPage />,
|
||||
});
|
||||
|
||||
const oauthAuthorizeRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'oauthAuthorize',
|
||||
path: Routes.OAUTH_AUTHORIZE,
|
||||
onEnter: () => {
|
||||
const current = window.location.pathname + window.location.search;
|
||||
|
||||
if (!SessionManager.isInitialized) {
|
||||
void SessionManager.initialize().then(() => {
|
||||
if (!AuthenticationStore.isAuthenticated) {
|
||||
RouterUtils.replaceWith(`${Routes.LOGIN}?redirect_to=${encodeURIComponent(current)}`);
|
||||
}
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!AuthenticationStore.isAuthenticated) {
|
||||
return new Redirect(`${Routes.LOGIN}?redirect_to=${encodeURIComponent(current)}`);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
component: () => <OAuthAuthorizePage />,
|
||||
});
|
||||
|
||||
const inviteRegisterRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'inviteRegister',
|
||||
path: '/invite/:code',
|
||||
onEnter: whenAuthenticated((ctx) => {
|
||||
const code = ctx.params.code;
|
||||
if (code) {
|
||||
InviteActionCreators.openAcceptModal(code);
|
||||
}
|
||||
return new Redirect(Routes.ME);
|
||||
}),
|
||||
component: () => <InviteRegisterPage />,
|
||||
});
|
||||
|
||||
const inviteLoginRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'inviteLogin',
|
||||
path: '/invite/:code/login',
|
||||
onEnter: whenAuthenticated((ctx) => {
|
||||
const code = ctx.params.code;
|
||||
if (code) {
|
||||
InviteActionCreators.openAcceptModal(code);
|
||||
}
|
||||
return new Redirect(Routes.ME);
|
||||
}),
|
||||
component: () => <InviteLoginPage />,
|
||||
});
|
||||
|
||||
const giftRegisterRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'giftRegister',
|
||||
path: '/gift/:code',
|
||||
onEnter: whenAuthenticated((ctx) => {
|
||||
const code = ctx.params.code;
|
||||
if (code) {
|
||||
GiftActionCreators.openAcceptModal(code);
|
||||
}
|
||||
return new Redirect(Routes.ME);
|
||||
}),
|
||||
component: () => <GiftRegisterPage />,
|
||||
});
|
||||
|
||||
const giftLoginRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'giftLogin',
|
||||
path: '/gift/:code/login',
|
||||
onEnter: whenAuthenticated((ctx) => {
|
||||
const code = ctx.params.code;
|
||||
if (code) {
|
||||
GiftActionCreators.openAcceptModal(code);
|
||||
}
|
||||
return new Redirect(Routes.ME);
|
||||
}),
|
||||
component: () => <GiftLoginPage />,
|
||||
});
|
||||
|
||||
const forgotPasswordRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'forgotPassword',
|
||||
path: Routes.FORGOT_PASSWORD,
|
||||
onEnter: whenAuthenticated(() => new Redirect(Routes.ME)),
|
||||
component: () => <ForgotPasswordPage />,
|
||||
});
|
||||
|
||||
const resetPasswordRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'resetPassword',
|
||||
path: Routes.RESET_PASSWORD,
|
||||
component: () => <ResetPasswordPage />,
|
||||
});
|
||||
|
||||
const emailRevertRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'emailRevert',
|
||||
path: Routes.EMAIL_REVERT,
|
||||
component: () => <EmailRevertPage />,
|
||||
});
|
||||
|
||||
const verifyEmailRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'verifyEmail',
|
||||
path: Routes.VERIFY_EMAIL,
|
||||
component: () => <VerifyEmailPage />,
|
||||
});
|
||||
|
||||
const authorizeIPRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'authorizeIP',
|
||||
path: Routes.AUTHORIZE_IP,
|
||||
component: () => <AuthorizeIPPage />,
|
||||
});
|
||||
|
||||
const pendingVerificationRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'pendingVerification',
|
||||
path: Routes.PENDING_VERIFICATION,
|
||||
component: () => <PendingVerificationPage />,
|
||||
});
|
||||
|
||||
const reportRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'report',
|
||||
path: Routes.REPORT,
|
||||
component: () => <ReportPage />,
|
||||
});
|
||||
|
||||
const themeRegisterRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'themeRegister',
|
||||
path: Routes.THEME_REGISTER,
|
||||
onEnter: whenAuthenticated((ctx) => {
|
||||
const themeId = ctx.params.themeId;
|
||||
if (themeId) {
|
||||
ThemeActionCreators.openAcceptModal(themeId, i18n);
|
||||
}
|
||||
return new Redirect(Routes.ME);
|
||||
}),
|
||||
component: () => <ThemeRegisterPage />,
|
||||
});
|
||||
|
||||
const themeLoginRoute = createRoute({
|
||||
getParentRoute: () => authLayoutRoute,
|
||||
id: 'themeLogin',
|
||||
path: Routes.THEME_LOGIN,
|
||||
onEnter: whenAuthenticated((ctx) => {
|
||||
const themeId = ctx.params.themeId;
|
||||
if (themeId) {
|
||||
ThemeActionCreators.openAcceptModal(themeId, i18n);
|
||||
}
|
||||
return new Redirect(Routes.ME);
|
||||
}),
|
||||
component: () => <ThemeLoginPage />,
|
||||
});
|
||||
|
||||
export const authRouteTree = authLayoutRoute.addChildren([
|
||||
loginRoute,
|
||||
registerRoute,
|
||||
oauthAuthorizeRoute,
|
||||
inviteRegisterRoute,
|
||||
inviteLoginRoute,
|
||||
themeRegisterRoute,
|
||||
themeLoginRoute,
|
||||
forgotPasswordRoute,
|
||||
resetPasswordRoute,
|
||||
emailRevertRoute,
|
||||
verifyEmailRoute,
|
||||
authorizeIPRoute,
|
||||
pendingVerificationRoute,
|
||||
reportRoute,
|
||||
...(RuntimeConfigStore.isSelfHosted() ? [] : [giftRegisterRoute, giftLoginRoute]),
|
||||
]);
|
||||
27
fluxer_app/src/router/routes/index.tsx
Normal file
27
fluxer_app/src/router/routes/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type {RouteConfig} from '~/lib/router';
|
||||
import {appRouteTree} from '~/router/routes/appRoutes';
|
||||
import {authRouteTree} from '~/router/routes/authRoutes';
|
||||
import {homeRoute, notFoundRoute, rootRoute} from '~/router/routes/rootRoutes';
|
||||
|
||||
const routeTree = rootRoute.addChildren([homeRoute, notFoundRoute, authRouteTree, appRouteTree]);
|
||||
|
||||
export const buildRoutes = (): Array<RouteConfig> => routeTree.build();
|
||||
40
fluxer_app/src/router/routes/rootRoutes.tsx
Normal file
40
fluxer_app/src/router/routes/rootRoutes.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Fluxer Contributors
|
||||
*
|
||||
* This file is part of Fluxer.
|
||||
*
|
||||
* Fluxer is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Fluxer is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {NotFoundPage} from '~/components/pages/NotFoundPage';
|
||||
import {createRootRoute, createRoute, Redirect} from '~/lib/router';
|
||||
import {Routes} from '~/Routes';
|
||||
import {RootComponent} from '~/router/components/RootComponent';
|
||||
|
||||
export const rootRoute = createRootRoute({
|
||||
layout: ({children}) => <RootComponent>{children}</RootComponent>,
|
||||
});
|
||||
|
||||
export const notFoundRoute = createRoute({
|
||||
id: '__notFound',
|
||||
path: '/__notfound',
|
||||
component: () => <NotFoundPage />,
|
||||
});
|
||||
|
||||
export const homeRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
id: 'home',
|
||||
path: '/',
|
||||
onEnter: () => new Redirect(Routes.ME),
|
||||
});
|
||||
Reference in New Issue
Block a user