refactor project layouts around route boundaries (#45)

* refactor: align app layouts with route boundaries

* fix: honor auth redirect search in auth layout
This commit is contained in:
Ben Senescu 2026-03-24 17:50:35 -04:00 committed by GitHub
parent 4040a854a7
commit bfc6a97466
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 748 additions and 676 deletions

View File

@ -3,42 +3,12 @@ import { ChevronsUpDown, X } from "lucide-react";
import { getProjectNavItems } from "@/client/navigation/items";
interface SidebarProps {
currentPath: string;
projectId: string | null;
projectId: string;
onNavigate?: () => void;
onClose?: () => void;
}
export function Sidebar({
currentPath,
projectId,
onNavigate,
onClose,
}: SidebarProps) {
// If we don't have a projectId yet (e.g., root redirect hasn't fired),
// don't render nav links since we can't build the URLs.
if (!projectId) {
return (
<div className="sidebar w-64 border-r border-base-300 h-full bg-base-100 flex flex-col">
<div className="px-4 py-4 border-b border-base-300 flex items-center justify-between">
<span className="font-semibold text-base-content">OpenSEO</span>
{onClose && (
<button
onClick={onClose}
className="btn btn-ghost btn-sm btn-circle"
aria-label="Close sidebar"
>
<X className="h-5 w-5" />
</button>
)}
</div>
<div className="flex-1 flex items-center justify-center">
<span className="loading loading-spinner loading-sm" />
</div>
</div>
);
}
export function Sidebar({ projectId, onNavigate, onClose }: SidebarProps) {
const projectNavItems = getProjectNavItems(projectId);
return (
@ -73,25 +43,26 @@ export function Sidebar({
{/* Navigation */}
<nav className="flex-1 py-4 pl-3 overflow-y-auto">
{projectNavItems.map((item) => {
const { icon: Icon, matchSegment, ...linkProps } = item;
const isActive = currentPath.includes(matchSegment);
const { icon: Icon, ...linkProps } = item;
return (
<Link
key={linkProps.to}
{...linkProps}
onClick={onNavigate}
className={`relative flex items-center gap-3 pl-4 pr-4 py-2 text-sm transition-colors ${
isActive
? "text-base-content font-medium"
: "text-base-content/60 hover:text-base-content hover:bg-base-200"
}`}
activeOptions={{ exact: false, includeSearch: false }}
className="relative flex items-center gap-3 px-4 py-2 text-sm text-base-content/60 transition-colors hover:bg-base-200 hover:text-base-content"
activeProps={{ className: "text-base-content font-medium" }}
>
{isActive && (
<div className="absolute left-0 top-1 bottom-1 w-[3px] bg-primary rounded-r-full" />
{({ isActive }: { isActive: boolean }) => (
<>
{isActive ? (
<div className="absolute left-0 top-1 bottom-1 w-[3px] rounded-r-full bg-primary" />
) : null}
<Icon className="h-5 w-5" />
{item.label}
</>
)}
<Icon className="h-5 w-5" />
{item.label}
</Link>
);
})}

View File

@ -1,5 +1,5 @@
import { useEffect } from "react";
import { getCurrentAuthRedirect } from "@/lib/auth-redirect";
import { getSignInHref, getSignInHrefForLocation } from "@/lib/auth-redirect";
import { isHostedClientAuthMode } from "@/lib/auth-mode";
type UnauthenticatedErrorCardProps = {
@ -7,22 +7,15 @@ type UnauthenticatedErrorCardProps = {
onRetry?: () => void;
};
function getSignInHref(redirectTo: string) {
return redirectTo === "/"
? "/sign-in"
: `/sign-in?redirect=${encodeURIComponent(redirectTo)}`;
}
export function UnauthenticatedErrorCard({
message,
onRetry,
}: UnauthenticatedErrorCardProps) {
const redirectTo =
typeof window === "undefined"
? "/"
: getCurrentAuthRedirect(window.location);
const isHostedMode = isHostedClientAuthMode();
const signInHref = getSignInHref(redirectTo);
const signInHref =
typeof window === "undefined"
? getSignInHref("/")
: getSignInHrefForLocation(window.location);
useEffect(() => {
if (typeof window === "undefined" || !isHostedMode) {

View File

@ -1,5 +1,3 @@
import { useEffect } from "react";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { normalizeAuthRedirect } from "@/lib/auth-redirect";
import { useSession } from "@/lib/auth-client";
@ -10,19 +8,10 @@ export const authRedirectSearchSchema = z.object({
});
export function useAuthPageState(redirect: string | undefined) {
const navigate = useNavigate();
const redirectTo = normalizeAuthRedirect(redirect);
const { data: session, isPending: isSessionPending } = useSession();
const { isPending: isSessionPending } = useSession();
const isHostedMode = isHostedClientAuthMode();
useEffect(() => {
if (!session?.user?.id) {
return;
}
void navigate({ href: redirectTo });
}, [navigate, redirectTo, session?.user?.id]);
return {
redirectTo,
isHostedMode,
@ -30,10 +19,6 @@ export function useAuthPageState(redirect: string | undefined) {
};
}
export function getAuthLinkSearch(redirectTo: string) {
return redirectTo === "/" ? {} : { redirect: redirectTo };
}
export function getFieldError(errors: unknown[]) {
return typeof errors[0] === "string" ? errors[0] : null;
}

View File

@ -1,30 +1,132 @@
import * as React from "react";
import { Link, Outlet } from "@tanstack/react-router";
import { Link, useLocation } from "@tanstack/react-router";
import { ChevronsUpDown, Menu, User } from "lucide-react";
import { getProjectNavItems } from "@/client/navigation/items";
import {
AlertTriangle,
ChevronsUpDown,
ExternalLink,
Menu,
User,
} from "lucide-react";
import { Sidebar } from "@/client/components/Sidebar";
import {
dataforseoHelpLinkOptions,
getProjectNavItems,
} from "@/client/navigation/items";
import { getCurrentAuthRedirect } from "@/lib/auth-redirect";
AppContent,
MissingSeoSetupModal,
SeoApiStatusBanners,
} from "@/client/layout/AppShellParts";
import { getSignInHrefForLocation } from "@/lib/auth-redirect";
import { authClient, useSession } from "@/lib/auth-client";
import { isHostedClientAuthMode } from "@/lib/auth-mode";
import { getSeoApiKeyStatus } from "@/serverFunctions/config";
export function TopNav({
const DATAFORSEO_HELP_PATH = "/help/dataforseo-api-key";
export function AuthenticatedAppLayout({
children,
projectId,
}: {
children: React.ReactNode;
projectId?: string;
}) {
const location = useLocation();
const [drawerOpen, setDrawerOpen] = React.useState(false);
const setupModalRef = React.useRef<HTMLDivElement | null>(null);
const [isSeoApiKeyConfigured, setIsSeoApiKeyConfigured] = React.useState<
boolean | null
>(null);
const [seoApiKeyStatusError, setSeoApiKeyStatusError] = React.useState(false);
const [showMissingSeoApiKeyModal, setShowMissingSeoApiKeyModal] =
React.useState(false);
React.useEffect(() => {
let cancelled = false;
const checkSeoApiKeyStatus = async () => {
try {
const result = await getSeoApiKeyStatus();
if (cancelled) return;
setSeoApiKeyStatusError(false);
setIsSeoApiKeyConfigured(result.configured);
if (!result.configured) {
setShowMissingSeoApiKeyModal(true);
}
} catch {
if (cancelled) return;
setSeoApiKeyStatusError(true);
setIsSeoApiKeyConfigured(null);
setShowMissingSeoApiKeyModal(false);
}
};
void checkSeoApiKeyStatus();
return () => {
cancelled = true;
};
}, []);
const shouldShowMissingSeoApiKeyModal =
showMissingSeoApiKeyModal && location.pathname !== DATAFORSEO_HELP_PATH;
const shouldShowSeoApiWarning =
!seoApiKeyStatusError &&
isSeoApiKeyConfigured === false &&
!shouldShowMissingSeoApiKeyModal;
React.useEffect(() => {
if (!shouldShowMissingSeoApiKeyModal) return;
setupModalRef.current?.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setShowMissingSeoApiKeyModal(false);
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [shouldShowMissingSeoApiKeyModal]);
React.useEffect(() => {
if (!projectId) {
setDrawerOpen(false);
}
}, [projectId]);
return (
<div className="flex h-[100dvh] flex-col bg-base-200">
<TopNav
drawerOpen={drawerOpen}
projectId={projectId ?? null}
onOpenDrawer={() => setDrawerOpen(true)}
/>
<SeoApiStatusBanners
shouldShowSeoApiWarning={shouldShowSeoApiWarning}
seoApiKeyStatusError={seoApiKeyStatusError}
/>
<AppContent
drawerOpen={drawerOpen}
projectId={projectId ?? null}
onCloseDrawer={() => setDrawerOpen(false)}
>
{children}
</AppContent>
<MissingSeoSetupModal
ref={setupModalRef}
isOpen={shouldShowMissingSeoApiKeyModal}
onClose={() => setShowMissingSeoApiKeyModal(false)}
/>
</div>
);
}
function TopNav({
drawerOpen,
projectId,
pathname,
onOpenDrawer,
}: {
drawerOpen: boolean;
projectId: string | null;
pathname: string;
onOpenDrawer: () => void;
}) {
const isHostedMode = isHostedClientAuthMode();
@ -33,15 +135,17 @@ export function TopNav({
return (
<div className="navbar bg-base-100 border-b border-base-300 shrink-0 gap-2">
<div className="flex-none flex items-center md:hidden">
<button
type="button"
className="btn btn-square btn-ghost"
aria-label="Toggle sidebar"
aria-expanded={drawerOpen}
onClick={onOpenDrawer}
>
<Menu className="h-6 w-6" />
</button>
{projectId ? (
<button
type="button"
className="btn btn-square btn-ghost"
aria-label="Toggle sidebar"
aria-expanded={drawerOpen}
onClick={onOpenDrawer}
>
<Menu className="h-6 w-6" />
</button>
) : null}
<span className="font-semibold text-base-content ml-1">OpenSEO</span>
</div>
@ -51,17 +155,17 @@ export function TopNav({
</span>
{projectId
? projectNavItems.map((item) => {
const { icon: Icon, matchSegment, ...linkProps } = item;
const isActive = pathname.includes(matchSegment);
const { icon: Icon, ...linkProps } = item;
return (
<Link
key={linkProps.to}
{...linkProps}
className={`btn btn-sm gap-2 ${
isActive
? "bg-primary/10 text-primary font-medium border-transparent"
: "btn-ghost text-base-content/60 hover:text-base-content"
}`}
activeOptions={{ exact: false, includeSearch: false }}
className="btn btn-sm gap-2 btn-ghost text-base-content/60 hover:text-base-content"
activeProps={{
className:
"bg-primary/10 text-primary font-medium border-transparent hover:text-primary",
}}
>
<Icon className="h-4 w-4" />
{item.label}
@ -117,15 +221,11 @@ function HostedSessionActions({
}
const handleSignOut = () => {
const redirectTo = getCurrentAuthRedirect(window.location);
const signInHref = getSignInHrefForLocation(window.location);
void authClient.signOut({
fetchOptions: {
onSuccess: () => {
window.location.assign(
redirectTo === "/"
? "/sign-in"
: `/sign-in?redirect=${encodeURIComponent(redirectTo)}`,
);
window.location.assign(signInHref);
},
},
});
@ -165,162 +265,3 @@ function HostedSessionActions({
</div>
);
}
export function SeoApiStatusBanners({
shouldShowSeoApiWarning,
seoApiKeyStatusError,
}: {
shouldShowSeoApiWarning: boolean;
seoApiKeyStatusError: boolean;
}) {
return (
<>
{shouldShowSeoApiWarning ? (
<div className="shrink-0 px-4 py-2.5 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="alert alert-warning">
<AlertTriangle className="size-4 shrink-0" />
<span className="text-sm">
Setup needed: add your DataForSEO API key to use OpenSEO
features. See the quick steps on the{" "}
<Link
{...dataforseoHelpLinkOptions}
className="link link-primary font-medium"
>
help page
</Link>
.
</span>
</div>
</div>
</div>
) : null}
{seoApiKeyStatusError ? (
<div className="shrink-0 px-4 py-2.5 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="alert alert-info">
<AlertTriangle className="size-4 shrink-0" />
<span className="text-sm">
We could not verify your DataForSEO setup. If features are not
working, check the setup steps on the{" "}
<Link
{...dataforseoHelpLinkOptions}
className="link link-primary font-medium"
>
help page
</Link>
.
</span>
</div>
</div>
</div>
) : null}
</>
);
}
export function AppContent({
drawerOpen,
pathname,
projectId,
onCloseDrawer,
}: {
drawerOpen: boolean;
pathname: string;
projectId: string | null;
onCloseDrawer: () => void;
}) {
return (
<>
<div className="flex-1 min-h-0 md:hidden">
<div className="h-full overflow-auto">
<Outlet />
</div>
{drawerOpen ? (
<div className="fixed inset-0 z-50">
<button
type="button"
aria-label="Close sidebar"
className="absolute inset-0 bg-black/45"
onClick={onCloseDrawer}
/>
<div className="absolute left-0 top-0 h-full">
<Sidebar
currentPath={pathname}
projectId={projectId}
onNavigate={onCloseDrawer}
onClose={onCloseDrawer}
/>
</div>
</div>
) : null}
</div>
<div className="hidden md:block flex-1 min-h-0 overflow-auto">
<Outlet />
</div>
</>
);
}
export const MissingSeoSetupModal = React.forwardRef<
HTMLDivElement,
{
isOpen: boolean;
onClose: () => void;
}
>(({ isOpen, onClose }, ref) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div
ref={ref}
role="dialog"
aria-modal="true"
aria-labelledby="dataforseo-setup-title"
aria-describedby="dataforseo-setup-description"
tabIndex={-1}
className="w-full max-w-lg rounded-xl border border-base-300 bg-base-100 p-5 shadow-2xl"
>
<div className="flex items-start gap-3">
<div className="rounded-full bg-warning/20 p-2 text-warning">
<AlertTriangle className="size-5" />
</div>
<div className="space-y-2">
<h2
id="dataforseo-setup-title"
className="text-lg font-semibold text-base-content"
>
One quick setup step
</h2>
<p
id="dataforseo-setup-description"
className="text-sm text-base-content/75"
>
Add your DataForSEO API key to start using OpenSEO.
</p>
</div>
</div>
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<button type="button" className="btn btn-ghost" onClick={onClose}>
Dismiss
</button>
<Link
{...dataforseoHelpLinkOptions}
className="btn btn-primary"
onClick={onClose}
>
Open setup guide
<ExternalLink className="size-4" />
</Link>
</div>
</div>
</div>
);
});
MissingSeoSetupModal.displayName = "MissingSeoSetupModal";

View File

@ -0,0 +1,163 @@
import * as React from "react";
import { Link } from "@tanstack/react-router";
import { AlertTriangle, ExternalLink } from "lucide-react";
import { Sidebar } from "@/client/components/Sidebar";
import { dataforseoHelpLinkOptions } from "@/client/navigation/items";
function SeoApiStatusBanners({
shouldShowSeoApiWarning,
seoApiKeyStatusError,
}: {
shouldShowSeoApiWarning: boolean;
seoApiKeyStatusError: boolean;
}) {
return (
<>
{shouldShowSeoApiWarning ? (
<div className="shrink-0 px-4 py-2.5 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="alert alert-warning">
<AlertTriangle className="size-4 shrink-0" />
<span className="text-sm">
Setup needed: add your DataForSEO API key to use OpenSEO
features. See the quick steps on the{" "}
<Link
{...dataforseoHelpLinkOptions}
className="link link-primary font-medium"
>
help page
</Link>
.
</span>
</div>
</div>
</div>
) : null}
{seoApiKeyStatusError ? (
<div className="shrink-0 px-4 py-2.5 md:px-6">
<div className="mx-auto max-w-7xl">
<div className="alert alert-info">
<AlertTriangle className="size-4 shrink-0" />
<span className="text-sm">
We could not verify your DataForSEO setup. If features are not
working, check the setup steps on the{" "}
<Link
{...dataforseoHelpLinkOptions}
className="link link-primary font-medium"
>
help page
</Link>
.
</span>
</div>
</div>
</div>
) : null}
</>
);
}
function AppContent({
drawerOpen,
projectId,
onCloseDrawer,
children,
}: {
drawerOpen: boolean;
projectId: string | null;
onCloseDrawer: () => void;
children: React.ReactNode;
}) {
return (
<>
<div className="flex-1 min-h-0 md:hidden">
<div className="h-full overflow-auto">{children}</div>
{drawerOpen && projectId ? (
<div className="fixed inset-0 z-50">
<button
type="button"
aria-label="Close sidebar"
className="absolute inset-0 bg-black/45"
onClick={onCloseDrawer}
/>
<div className="absolute left-0 top-0 h-full">
<Sidebar
projectId={projectId}
onNavigate={onCloseDrawer}
onClose={onCloseDrawer}
/>
</div>
</div>
) : null}
</div>
<div className="hidden md:block flex-1 min-h-0 overflow-auto">
{children}
</div>
</>
);
}
const MissingSeoSetupModal = React.forwardRef<
HTMLDivElement,
{
isOpen: boolean;
onClose: () => void;
}
>(({ isOpen, onClose }, ref) => {
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div
ref={ref}
role="dialog"
aria-modal="true"
aria-labelledby="dataforseo-setup-title"
aria-describedby="dataforseo-setup-description"
tabIndex={-1}
className="w-full max-w-lg rounded-xl border border-base-300 bg-base-100 p-5 shadow-2xl"
>
<div className="flex items-start gap-3">
<div className="rounded-full bg-warning/20 p-2 text-warning">
<AlertTriangle className="size-5" />
</div>
<div className="space-y-2">
<h2
id="dataforseo-setup-title"
className="text-lg font-semibold text-base-content"
>
One quick setup step
</h2>
<p
id="dataforseo-setup-description"
className="text-sm text-base-content/75"
>
Add your DataForSEO API key to start using OpenSEO.
</p>
</div>
</div>
<div className="mt-5 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
<button type="button" className="btn btn-ghost" onClick={onClose}>
Dismiss
</button>
<Link
{...dataforseoHelpLinkOptions}
className="btn btn-primary"
onClick={onClose}
>
Open setup guide
<ExternalLink className="size-4" />
</Link>
</div>
</div>
</div>
);
});
MissingSeoSetupModal.displayName = "MissingSeoSetupModal";
export { AppContent, MissingSeoSetupModal, SeoApiStatusBanners };

View File

@ -6,12 +6,32 @@ export function normalizeAuthRedirect(value: string | null | undefined) {
return value;
}
export function getCurrentAuthRedirect(location: {
export function getCurrentAuthRedirectFromHref(href: string) {
const url = new URL(href, "https://openseo.local");
return normalizeAuthRedirect(`${url.pathname}${url.search}${url.hash}`);
}
export function getSignInSearch(redirectTo: string) {
return redirectTo === "/" ? {} : { redirect: redirectTo };
}
export function getSignInHref(redirectTo: string) {
const search = getSignInSearch(redirectTo);
if (!("redirect" in search)) {
return "/sign-in";
}
return `/sign-in?redirect=${encodeURIComponent(search.redirect ?? "/")}`;
}
export function getSignInHrefForLocation(location: {
pathname: string;
search: string;
hash?: string;
}) {
return normalizeAuthRedirect(
`${location.pathname}${location.search}${location.hash ?? ""}`,
return getSignInHref(
normalizeAuthRedirect(
`${location.pathname}${location.search}${location.hash ?? ""}`,
),
);
}

View File

@ -10,36 +10,41 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as AuthRouteImport } from './routes/_auth'
import { Route as IndexRouteImport } from './routes/index'
import { Route as HelpDataforseoApiKeyRouteImport } from './routes/help/dataforseo-api-key'
import { Route as ProjectRouteRouteImport } from './routes/_project/route'
import { Route as AppRouteRouteImport } from './routes/_app/route'
import { Route as AppIndexRouteImport } from './routes/_app/index'
import { Route as AuthSignUpRouteImport } from './routes/_auth.sign-up'
import { Route as AuthSignInRouteImport } from './routes/_auth.sign-in'
import { Route as PProjectIdRouteRouteImport } from './routes/p/$projectId/route'
import { Route as PProjectIdIndexRouteImport } from './routes/p/$projectId/index'
import { Route as PProjectIdSavedRouteImport } from './routes/p/$projectId/saved'
import { Route as PProjectIdKeywordsRouteImport } from './routes/p/$projectId/keywords'
import { Route as PProjectIdDomainRouteImport } from './routes/p/$projectId/domain'
import { Route as PProjectIdBacklinksRouteImport } from './routes/p/$projectId/backlinks'
import { Route as PProjectIdAuditRouteImport } from './routes/p/$projectId/audit'
import { Route as PProjectIdAiRouteImport } from './routes/p/$projectId/ai'
import { Route as ApiAuthSplatRouteImport } from './routes/api/auth/$'
import { Route as PProjectIdAuditIndexRouteImport } from './routes/p/$projectId/audit/index'
import { Route as PProjectIdAuditIssuesResultIdRouteImport } from './routes/p/$projectId/audit/issues/$resultId'
import { Route as AppHelpDataforseoApiKeyRouteImport } from './routes/_app/help/dataforseo-api-key'
import { Route as ProjectPProjectIdRouteRouteImport } from './routes/_project/p/$projectId/route'
import { Route as ProjectPProjectIdIndexRouteImport } from './routes/_project/p/$projectId/index'
import { Route as ProjectPProjectIdSavedRouteImport } from './routes/_project/p/$projectId/saved'
import { Route as ProjectPProjectIdKeywordsRouteImport } from './routes/_project/p/$projectId/keywords'
import { Route as ProjectPProjectIdDomainRouteImport } from './routes/_project/p/$projectId/domain'
import { Route as ProjectPProjectIdBacklinksRouteImport } from './routes/_project/p/$projectId/backlinks'
import { Route as ProjectPProjectIdAuditRouteImport } from './routes/_project/p/$projectId/audit'
import { Route as ProjectPProjectIdAiRouteImport } from './routes/_project/p/$projectId/ai'
import { Route as ProjectPProjectIdAuditIndexRouteImport } from './routes/_project/p/$projectId/audit/index'
import { Route as ProjectPProjectIdAuditIssuesResultIdRouteImport } from './routes/_project/p/$projectId/audit/issues/$resultId'
const AuthRoute = AuthRouteImport.update({
id: '/_auth',
getParentRoute: () => rootRouteImport,
} as any)
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
const ProjectRouteRoute = ProjectRouteRouteImport.update({
id: '/_project',
getParentRoute: () => rootRouteImport,
} as any)
const HelpDataforseoApiKeyRoute = HelpDataforseoApiKeyRouteImport.update({
id: '/help/dataforseo-api-key',
path: '/help/dataforseo-api-key',
const AppRouteRoute = AppRouteRouteImport.update({
id: '/_app',
getParentRoute: () => rootRouteImport,
} as any)
const AppIndexRoute = AppIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => AppRouteRoute,
} as any)
const AuthSignUpRoute = AuthSignUpRouteImport.update({
id: '/sign-up',
path: '/sign-up',
@ -50,121 +55,131 @@ const AuthSignInRoute = AuthSignInRouteImport.update({
path: '/sign-in',
getParentRoute: () => AuthRoute,
} as any)
const PProjectIdRouteRoute = PProjectIdRouteRouteImport.update({
id: '/p/$projectId',
path: '/p/$projectId',
getParentRoute: () => rootRouteImport,
} as any)
const PProjectIdIndexRoute = PProjectIdIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdSavedRoute = PProjectIdSavedRouteImport.update({
id: '/saved',
path: '/saved',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdKeywordsRoute = PProjectIdKeywordsRouteImport.update({
id: '/keywords',
path: '/keywords',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdDomainRoute = PProjectIdDomainRouteImport.update({
id: '/domain',
path: '/domain',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdBacklinksRoute = PProjectIdBacklinksRouteImport.update({
id: '/backlinks',
path: '/backlinks',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdAuditRoute = PProjectIdAuditRouteImport.update({
id: '/audit',
path: '/audit',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const PProjectIdAiRoute = PProjectIdAiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => PProjectIdRouteRoute,
} as any)
const ApiAuthSplatRoute = ApiAuthSplatRouteImport.update({
id: '/api/auth/$',
path: '/api/auth/$',
getParentRoute: () => rootRouteImport,
} as any)
const PProjectIdAuditIndexRoute = PProjectIdAuditIndexRouteImport.update({
const AppHelpDataforseoApiKeyRoute = AppHelpDataforseoApiKeyRouteImport.update({
id: '/help/dataforseo-api-key',
path: '/help/dataforseo-api-key',
getParentRoute: () => AppRouteRoute,
} as any)
const ProjectPProjectIdRouteRoute = ProjectPProjectIdRouteRouteImport.update({
id: '/p/$projectId',
path: '/p/$projectId',
getParentRoute: () => ProjectRouteRoute,
} as any)
const ProjectPProjectIdIndexRoute = ProjectPProjectIdIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => PProjectIdAuditRoute,
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const PProjectIdAuditIssuesResultIdRoute =
PProjectIdAuditIssuesResultIdRouteImport.update({
const ProjectPProjectIdSavedRoute = ProjectPProjectIdSavedRouteImport.update({
id: '/saved',
path: '/saved',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdKeywordsRoute =
ProjectPProjectIdKeywordsRouteImport.update({
id: '/keywords',
path: '/keywords',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdDomainRoute = ProjectPProjectIdDomainRouteImport.update({
id: '/domain',
path: '/domain',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdBacklinksRoute =
ProjectPProjectIdBacklinksRouteImport.update({
id: '/backlinks',
path: '/backlinks',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdAuditRoute = ProjectPProjectIdAuditRouteImport.update({
id: '/audit',
path: '/audit',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdAiRoute = ProjectPProjectIdAiRouteImport.update({
id: '/ai',
path: '/ai',
getParentRoute: () => ProjectPProjectIdRouteRoute,
} as any)
const ProjectPProjectIdAuditIndexRoute =
ProjectPProjectIdAuditIndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => ProjectPProjectIdAuditRoute,
} as any)
const ProjectPProjectIdAuditIssuesResultIdRoute =
ProjectPProjectIdAuditIssuesResultIdRouteImport.update({
id: '/issues/$resultId',
path: '/issues/$resultId',
getParentRoute: () => PProjectIdAuditRoute,
getParentRoute: () => ProjectPProjectIdAuditRoute,
} as any)
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/p/$projectId': typeof PProjectIdRouteRouteWithChildren
'/': typeof AppIndexRoute
'/sign-in': typeof AuthSignInRoute
'/sign-up': typeof AuthSignUpRoute
'/help/dataforseo-api-key': typeof HelpDataforseoApiKeyRoute
'/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/p/$projectId/ai': typeof PProjectIdAiRoute
'/p/$projectId/audit': typeof PProjectIdAuditRouteWithChildren
'/p/$projectId/backlinks': typeof PProjectIdBacklinksRoute
'/p/$projectId/domain': typeof PProjectIdDomainRoute
'/p/$projectId/keywords': typeof PProjectIdKeywordsRoute
'/p/$projectId/saved': typeof PProjectIdSavedRoute
'/p/$projectId/': typeof PProjectIdIndexRoute
'/p/$projectId/audit/': typeof PProjectIdAuditIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof PProjectIdAuditIssuesResultIdRoute
'/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
'/p/$projectId/': typeof ProjectPProjectIdIndexRoute
'/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/': typeof AppIndexRoute
'/sign-in': typeof AuthSignInRoute
'/sign-up': typeof AuthSignUpRoute
'/help/dataforseo-api-key': typeof HelpDataforseoApiKeyRoute
'/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/p/$projectId/ai': typeof PProjectIdAiRoute
'/p/$projectId/backlinks': typeof PProjectIdBacklinksRoute
'/p/$projectId/domain': typeof PProjectIdDomainRoute
'/p/$projectId/keywords': typeof PProjectIdKeywordsRoute
'/p/$projectId/saved': typeof PProjectIdSavedRoute
'/p/$projectId': typeof PProjectIdIndexRoute
'/p/$projectId/audit': typeof PProjectIdAuditIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof PProjectIdAuditIssuesResultIdRoute
'/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
'/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
'/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
'/p/$projectId': typeof ProjectPProjectIdIndexRoute
'/p/$projectId/audit': typeof ProjectPProjectIdAuditIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/_app': typeof AppRouteRouteWithChildren
'/_project': typeof ProjectRouteRouteWithChildren
'/_auth': typeof AuthRouteWithChildren
'/p/$projectId': typeof PProjectIdRouteRouteWithChildren
'/_auth/sign-in': typeof AuthSignInRoute
'/_auth/sign-up': typeof AuthSignUpRoute
'/help/dataforseo-api-key': typeof HelpDataforseoApiKeyRoute
'/_app/': typeof AppIndexRoute
'/_project/p/$projectId': typeof ProjectPProjectIdRouteRouteWithChildren
'/_app/help/dataforseo-api-key': typeof AppHelpDataforseoApiKeyRoute
'/api/auth/$': typeof ApiAuthSplatRoute
'/p/$projectId/ai': typeof PProjectIdAiRoute
'/p/$projectId/audit': typeof PProjectIdAuditRouteWithChildren
'/p/$projectId/backlinks': typeof PProjectIdBacklinksRoute
'/p/$projectId/domain': typeof PProjectIdDomainRoute
'/p/$projectId/keywords': typeof PProjectIdKeywordsRoute
'/p/$projectId/saved': typeof PProjectIdSavedRoute
'/p/$projectId/': typeof PProjectIdIndexRoute
'/p/$projectId/audit/': typeof PProjectIdAuditIndexRoute
'/p/$projectId/audit/issues/$resultId': typeof PProjectIdAuditIssuesResultIdRoute
'/_project/p/$projectId/ai': typeof ProjectPProjectIdAiRoute
'/_project/p/$projectId/audit': typeof ProjectPProjectIdAuditRouteWithChildren
'/_project/p/$projectId/backlinks': typeof ProjectPProjectIdBacklinksRoute
'/_project/p/$projectId/domain': typeof ProjectPProjectIdDomainRoute
'/_project/p/$projectId/keywords': typeof ProjectPProjectIdKeywordsRoute
'/_project/p/$projectId/saved': typeof ProjectPProjectIdSavedRoute
'/_project/p/$projectId/': typeof ProjectPProjectIdIndexRoute
'/_project/p/$projectId/audit/': typeof ProjectPProjectIdAuditIndexRoute
'/_project/p/$projectId/audit/issues/$resultId': typeof ProjectPProjectIdAuditIssuesResultIdRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/'
| '/p/$projectId'
| '/sign-in'
| '/sign-up'
| '/p/$projectId'
| '/help/dataforseo-api-key'
| '/api/auth/$'
| '/p/$projectId/ai'
@ -193,29 +208,30 @@ export interface FileRouteTypes {
| '/p/$projectId/audit/issues/$resultId'
id:
| '__root__'
| '/'
| '/_app'
| '/_project'
| '/_auth'
| '/p/$projectId'
| '/_auth/sign-in'
| '/_auth/sign-up'
| '/help/dataforseo-api-key'
| '/_app/'
| '/_project/p/$projectId'
| '/_app/help/dataforseo-api-key'
| '/api/auth/$'
| '/p/$projectId/ai'
| '/p/$projectId/audit'
| '/p/$projectId/backlinks'
| '/p/$projectId/domain'
| '/p/$projectId/keywords'
| '/p/$projectId/saved'
| '/p/$projectId/'
| '/p/$projectId/audit/'
| '/p/$projectId/audit/issues/$resultId'
| '/_project/p/$projectId/ai'
| '/_project/p/$projectId/audit'
| '/_project/p/$projectId/backlinks'
| '/_project/p/$projectId/domain'
| '/_project/p/$projectId/keywords'
| '/_project/p/$projectId/saved'
| '/_project/p/$projectId/'
| '/_project/p/$projectId/audit/'
| '/_project/p/$projectId/audit/issues/$resultId'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AppRouteRoute: typeof AppRouteRouteWithChildren
ProjectRouteRoute: typeof ProjectRouteRouteWithChildren
AuthRoute: typeof AuthRouteWithChildren
PProjectIdRouteRoute: typeof PProjectIdRouteRouteWithChildren
HelpDataforseoApiKeyRoute: typeof HelpDataforseoApiKeyRoute
ApiAuthSplatRoute: typeof ApiAuthSplatRoute
}
@ -228,20 +244,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
'/_project': {
id: '/_project'
path: ''
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
preLoaderRoute: typeof ProjectRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/help/dataforseo-api-key': {
id: '/help/dataforseo-api-key'
path: '/help/dataforseo-api-key'
fullPath: '/help/dataforseo-api-key'
preLoaderRoute: typeof HelpDataforseoApiKeyRouteImport
'/_app': {
id: '/_app'
path: ''
fullPath: '/'
preLoaderRoute: typeof AppRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/_app/': {
id: '/_app/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof AppIndexRouteImport
parentRoute: typeof AppRouteRoute
}
'/_auth/sign-up': {
id: '/_auth/sign-up'
path: '/sign-up'
@ -256,62 +279,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthSignInRouteImport
parentRoute: typeof AuthRoute
}
'/p/$projectId': {
id: '/p/$projectId'
path: '/p/$projectId'
fullPath: '/p/$projectId'
preLoaderRoute: typeof PProjectIdRouteRouteImport
parentRoute: typeof rootRouteImport
}
'/p/$projectId/': {
id: '/p/$projectId/'
path: '/'
fullPath: '/p/$projectId/'
preLoaderRoute: typeof PProjectIdIndexRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/saved': {
id: '/p/$projectId/saved'
path: '/saved'
fullPath: '/p/$projectId/saved'
preLoaderRoute: typeof PProjectIdSavedRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/keywords': {
id: '/p/$projectId/keywords'
path: '/keywords'
fullPath: '/p/$projectId/keywords'
preLoaderRoute: typeof PProjectIdKeywordsRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/domain': {
id: '/p/$projectId/domain'
path: '/domain'
fullPath: '/p/$projectId/domain'
preLoaderRoute: typeof PProjectIdDomainRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/backlinks': {
id: '/p/$projectId/backlinks'
path: '/backlinks'
fullPath: '/p/$projectId/backlinks'
preLoaderRoute: typeof PProjectIdBacklinksRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/audit': {
id: '/p/$projectId/audit'
path: '/audit'
fullPath: '/p/$projectId/audit'
preLoaderRoute: typeof PProjectIdAuditRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/p/$projectId/ai': {
id: '/p/$projectId/ai'
path: '/ai'
fullPath: '/p/$projectId/ai'
preLoaderRoute: typeof PProjectIdAiRouteImport
parentRoute: typeof PProjectIdRouteRoute
}
'/api/auth/$': {
id: '/api/auth/$'
path: '/api/auth/$'
@ -319,23 +286,155 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof ApiAuthSplatRouteImport
parentRoute: typeof rootRouteImport
}
'/p/$projectId/audit/': {
id: '/p/$projectId/audit/'
'/_app/help/dataforseo-api-key': {
id: '/_app/help/dataforseo-api-key'
path: '/help/dataforseo-api-key'
fullPath: '/help/dataforseo-api-key'
preLoaderRoute: typeof AppHelpDataforseoApiKeyRouteImport
parentRoute: typeof AppRouteRoute
}
'/_project/p/$projectId': {
id: '/_project/p/$projectId'
path: '/p/$projectId'
fullPath: '/p/$projectId'
preLoaderRoute: typeof ProjectPProjectIdRouteRouteImport
parentRoute: typeof ProjectRouteRoute
}
'/_project/p/$projectId/': {
id: '/_project/p/$projectId/'
path: '/'
fullPath: '/p/$projectId/'
preLoaderRoute: typeof ProjectPProjectIdIndexRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/saved': {
id: '/_project/p/$projectId/saved'
path: '/saved'
fullPath: '/p/$projectId/saved'
preLoaderRoute: typeof ProjectPProjectIdSavedRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/keywords': {
id: '/_project/p/$projectId/keywords'
path: '/keywords'
fullPath: '/p/$projectId/keywords'
preLoaderRoute: typeof ProjectPProjectIdKeywordsRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/domain': {
id: '/_project/p/$projectId/domain'
path: '/domain'
fullPath: '/p/$projectId/domain'
preLoaderRoute: typeof ProjectPProjectIdDomainRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/backlinks': {
id: '/_project/p/$projectId/backlinks'
path: '/backlinks'
fullPath: '/p/$projectId/backlinks'
preLoaderRoute: typeof ProjectPProjectIdBacklinksRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/audit': {
id: '/_project/p/$projectId/audit'
path: '/audit'
fullPath: '/p/$projectId/audit'
preLoaderRoute: typeof ProjectPProjectIdAuditRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/ai': {
id: '/_project/p/$projectId/ai'
path: '/ai'
fullPath: '/p/$projectId/ai'
preLoaderRoute: typeof ProjectPProjectIdAiRouteImport
parentRoute: typeof ProjectPProjectIdRouteRoute
}
'/_project/p/$projectId/audit/': {
id: '/_project/p/$projectId/audit/'
path: '/'
fullPath: '/p/$projectId/audit/'
preLoaderRoute: typeof PProjectIdAuditIndexRouteImport
parentRoute: typeof PProjectIdAuditRoute
preLoaderRoute: typeof ProjectPProjectIdAuditIndexRouteImport
parentRoute: typeof ProjectPProjectIdAuditRoute
}
'/p/$projectId/audit/issues/$resultId': {
id: '/p/$projectId/audit/issues/$resultId'
'/_project/p/$projectId/audit/issues/$resultId': {
id: '/_project/p/$projectId/audit/issues/$resultId'
path: '/issues/$resultId'
fullPath: '/p/$projectId/audit/issues/$resultId'
preLoaderRoute: typeof PProjectIdAuditIssuesResultIdRouteImport
parentRoute: typeof PProjectIdAuditRoute
preLoaderRoute: typeof ProjectPProjectIdAuditIssuesResultIdRouteImport
parentRoute: typeof ProjectPProjectIdAuditRoute
}
}
}
interface AppRouteRouteChildren {
AppIndexRoute: typeof AppIndexRoute
AppHelpDataforseoApiKeyRoute: typeof AppHelpDataforseoApiKeyRoute
}
const AppRouteRouteChildren: AppRouteRouteChildren = {
AppIndexRoute: AppIndexRoute,
AppHelpDataforseoApiKeyRoute: AppHelpDataforseoApiKeyRoute,
}
const AppRouteRouteWithChildren = AppRouteRoute._addFileChildren(
AppRouteRouteChildren,
)
interface ProjectPProjectIdAuditRouteChildren {
ProjectPProjectIdAuditIndexRoute: typeof ProjectPProjectIdAuditIndexRoute
ProjectPProjectIdAuditIssuesResultIdRoute: typeof ProjectPProjectIdAuditIssuesResultIdRoute
}
const ProjectPProjectIdAuditRouteChildren: ProjectPProjectIdAuditRouteChildren =
{
ProjectPProjectIdAuditIndexRoute: ProjectPProjectIdAuditIndexRoute,
ProjectPProjectIdAuditIssuesResultIdRoute:
ProjectPProjectIdAuditIssuesResultIdRoute,
}
const ProjectPProjectIdAuditRouteWithChildren =
ProjectPProjectIdAuditRoute._addFileChildren(
ProjectPProjectIdAuditRouteChildren,
)
interface ProjectPProjectIdRouteRouteChildren {
ProjectPProjectIdAiRoute: typeof ProjectPProjectIdAiRoute
ProjectPProjectIdAuditRoute: typeof ProjectPProjectIdAuditRouteWithChildren
ProjectPProjectIdBacklinksRoute: typeof ProjectPProjectIdBacklinksRoute
ProjectPProjectIdDomainRoute: typeof ProjectPProjectIdDomainRoute
ProjectPProjectIdKeywordsRoute: typeof ProjectPProjectIdKeywordsRoute
ProjectPProjectIdSavedRoute: typeof ProjectPProjectIdSavedRoute
ProjectPProjectIdIndexRoute: typeof ProjectPProjectIdIndexRoute
}
const ProjectPProjectIdRouteRouteChildren: ProjectPProjectIdRouteRouteChildren =
{
ProjectPProjectIdAiRoute: ProjectPProjectIdAiRoute,
ProjectPProjectIdAuditRoute: ProjectPProjectIdAuditRouteWithChildren,
ProjectPProjectIdBacklinksRoute: ProjectPProjectIdBacklinksRoute,
ProjectPProjectIdDomainRoute: ProjectPProjectIdDomainRoute,
ProjectPProjectIdKeywordsRoute: ProjectPProjectIdKeywordsRoute,
ProjectPProjectIdSavedRoute: ProjectPProjectIdSavedRoute,
ProjectPProjectIdIndexRoute: ProjectPProjectIdIndexRoute,
}
const ProjectPProjectIdRouteRouteWithChildren =
ProjectPProjectIdRouteRoute._addFileChildren(
ProjectPProjectIdRouteRouteChildren,
)
interface ProjectRouteRouteChildren {
ProjectPProjectIdRouteRoute: typeof ProjectPProjectIdRouteRouteWithChildren
}
const ProjectRouteRouteChildren: ProjectRouteRouteChildren = {
ProjectPProjectIdRouteRoute: ProjectPProjectIdRouteRouteWithChildren,
}
const ProjectRouteRouteWithChildren = ProjectRouteRoute._addFileChildren(
ProjectRouteRouteChildren,
)
interface AuthRouteChildren {
AuthSignInRoute: typeof AuthSignInRoute
AuthSignUpRoute: typeof AuthSignUpRoute
@ -348,49 +447,10 @@ const AuthRouteChildren: AuthRouteChildren = {
const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
interface PProjectIdAuditRouteChildren {
PProjectIdAuditIndexRoute: typeof PProjectIdAuditIndexRoute
PProjectIdAuditIssuesResultIdRoute: typeof PProjectIdAuditIssuesResultIdRoute
}
const PProjectIdAuditRouteChildren: PProjectIdAuditRouteChildren = {
PProjectIdAuditIndexRoute: PProjectIdAuditIndexRoute,
PProjectIdAuditIssuesResultIdRoute: PProjectIdAuditIssuesResultIdRoute,
}
const PProjectIdAuditRouteWithChildren = PProjectIdAuditRoute._addFileChildren(
PProjectIdAuditRouteChildren,
)
interface PProjectIdRouteRouteChildren {
PProjectIdAiRoute: typeof PProjectIdAiRoute
PProjectIdAuditRoute: typeof PProjectIdAuditRouteWithChildren
PProjectIdBacklinksRoute: typeof PProjectIdBacklinksRoute
PProjectIdDomainRoute: typeof PProjectIdDomainRoute
PProjectIdKeywordsRoute: typeof PProjectIdKeywordsRoute
PProjectIdSavedRoute: typeof PProjectIdSavedRoute
PProjectIdIndexRoute: typeof PProjectIdIndexRoute
}
const PProjectIdRouteRouteChildren: PProjectIdRouteRouteChildren = {
PProjectIdAiRoute: PProjectIdAiRoute,
PProjectIdAuditRoute: PProjectIdAuditRouteWithChildren,
PProjectIdBacklinksRoute: PProjectIdBacklinksRoute,
PProjectIdDomainRoute: PProjectIdDomainRoute,
PProjectIdKeywordsRoute: PProjectIdKeywordsRoute,
PProjectIdSavedRoute: PProjectIdSavedRoute,
PProjectIdIndexRoute: PProjectIdIndexRoute,
}
const PProjectIdRouteRouteWithChildren = PProjectIdRouteRoute._addFileChildren(
PProjectIdRouteRouteChildren,
)
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AppRouteRoute: AppRouteRouteWithChildren,
ProjectRouteRoute: ProjectRouteRouteWithChildren,
AuthRoute: AuthRouteWithChildren,
PProjectIdRouteRoute: PProjectIdRouteRouteWithChildren,
HelpDataforseoApiKeyRoute: HelpDataforseoApiKeyRoute,
ApiAuthSplatRoute: ApiAuthSplatRoute,
}
export const routeTree = rootRouteImport

View File

@ -5,28 +5,16 @@ import {
Outlet,
Scripts,
createRootRoute,
useLocation,
useMatches,
} from "@tanstack/react-router";
import { TanStackRouterDevtoolsPanel } from "@tanstack/react-router-devtools";
import { TanStackDevtools } from "@tanstack/react-devtools";
import { QueryClientProvider } from "@tanstack/react-query";
import * as React from "react";
import { useState } from "react";
import { DefaultCatchBoundary } from "@/client/components/DefaultCatchBoundary";
import { NotFound } from "@/client/components/NotFound";
import appCss from "@/client/styles/app.css?url";
import { Toaster } from "sonner";
import { queryClient } from "@/client/tanstack-db";
import { getSeoApiKeyStatus } from "@/serverFunctions/config";
import {
AppContent,
MissingSeoSetupModal,
SeoApiStatusBanners,
TopNav,
} from "@/client/layout/AppShell";
const DATAFORSEO_HELP_PATH = "/help/dataforseo-api-key";
export const Route = createRootRoute({
head: () => ({
@ -77,112 +65,7 @@ export const Route = createRootRoute({
});
function AppLayout() {
const isAuthLayout = useMatches({
select: (matches) => matches.some((match) => match.routeId === "/_auth"),
});
if (isAuthLayout) {
return <Outlet />;
}
return <AppShellLayout />;
}
function AppShellLayout() {
const location = useLocation();
const [drawerOpen, setDrawerOpen] = useState(false);
const setupModalRef = React.useRef<HTMLDivElement | null>(null);
const [isSeoApiKeyConfigured, setIsSeoApiKeyConfigured] = useState<
boolean | null
>(null);
const [seoApiKeyStatusError, setSeoApiKeyStatusError] = useState(false);
const [showMissingSeoApiKeyModal, setShowMissingSeoApiKeyModal] =
useState(false);
const projectIdMatch = location.pathname.match(/^\/p\/([^/]+)/);
const projectId = projectIdMatch?.[1] ?? null;
React.useEffect(() => {
let cancelled = false;
const checkSeoApiKeyStatus = async () => {
try {
const result = await getSeoApiKeyStatus();
if (cancelled) return;
setSeoApiKeyStatusError(false);
setIsSeoApiKeyConfigured(result.configured);
if (!result.configured) {
setShowMissingSeoApiKeyModal(true);
}
} catch {
if (cancelled) return;
setSeoApiKeyStatusError(true);
setIsSeoApiKeyConfigured(null);
setShowMissingSeoApiKeyModal(false);
}
};
void checkSeoApiKeyStatus();
return () => {
cancelled = true;
};
}, []);
const shouldShowMissingSeoApiKeyModal =
showMissingSeoApiKeyModal && location.pathname !== DATAFORSEO_HELP_PATH;
const shouldShowSeoApiWarning =
!seoApiKeyStatusError &&
isSeoApiKeyConfigured === false &&
!shouldShowMissingSeoApiKeyModal;
React.useEffect(() => {
if (!shouldShowMissingSeoApiKeyModal) return;
setupModalRef.current?.focus();
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
setShowMissingSeoApiKeyModal(false);
}
};
window.addEventListener("keydown", onKeyDown);
return () => {
window.removeEventListener("keydown", onKeyDown);
};
}, [shouldShowMissingSeoApiKeyModal]);
return (
<div className="flex flex-col h-[100dvh] bg-base-200">
<TopNav
drawerOpen={drawerOpen}
projectId={projectId}
pathname={location.pathname}
onOpenDrawer={() => setDrawerOpen(true)}
/>
<SeoApiStatusBanners
shouldShowSeoApiWarning={shouldShowSeoApiWarning}
seoApiKeyStatusError={seoApiKeyStatusError}
/>
<AppContent
drawerOpen={drawerOpen}
pathname={location.pathname}
projectId={projectId}
onCloseDrawer={() => setDrawerOpen(false)}
/>
<MissingSeoSetupModal
ref={setupModalRef}
isOpen={shouldShowMissingSeoApiKeyModal}
onClose={() => setShowMissingSeoApiKeyModal(false)}
/>
</div>
);
return <Outlet />;
}
function RootDocument({ children }: { children: React.ReactNode }) {

View File

@ -2,7 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
const DATAFORSEO_API_ACCESS_URL = "https://app.dataforseo.com/api-access";
export const Route = createFileRoute("/help/dataforseo-api-key")({
export const Route = createFileRoute("/_app/help/dataforseo-api-key")({
component: DataforseoApiKeyHelpPage,
});

View File

@ -9,7 +9,7 @@ import {
import { AuthConfigErrorCard } from "@/client/components/AuthConfigErrorCard";
import { UnauthenticatedErrorCard } from "@/client/components/UnauthenticatedErrorCard";
export const Route = createFileRoute("/")({
export const Route = createFileRoute("/_app/")({
component: IndexRedirect,
});

14
src/routes/_app/route.tsx Normal file
View File

@ -0,0 +1,14 @@
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { AuthenticatedAppLayout } from "@/client/layout/AppShell";
export const Route = createFileRoute("/_app")({
component: AppRouteLayout,
});
function AppRouteLayout() {
return (
<AuthenticatedAppLayout>
<Outlet />
</AuthenticatedAppLayout>
);
}

View File

@ -3,10 +3,10 @@ import { Link, createFileRoute } from "@tanstack/react-router";
import {
AuthPageCard,
authRedirectSearchSchema,
getAuthLinkSearch,
useAuthPageState,
} from "@/client/features/auth/AuthPage";
import { authClient } from "@/lib/auth-client";
import { getSignInSearch } from "@/lib/auth-redirect";
import { z } from "zod";
const signInSchema = z.object({
@ -96,7 +96,7 @@ function SignInPage() {
Need an account?{" "}
<Link
to="/sign-up"
search={getAuthLinkSearch(redirectTo)}
search={getSignInSearch(redirectTo)}
className="link link-primary"
>
Create account

View File

@ -3,11 +3,11 @@ import { Link, createFileRoute } from "@tanstack/react-router";
import {
AuthPageCard,
authRedirectSearchSchema,
getAuthLinkSearch,
getFieldError,
useAuthPageState,
} from "@/client/features/auth/AuthPage";
import { authClient } from "@/lib/auth-client";
import { getSignInSearch } from "@/lib/auth-redirect";
import {
HOSTED_PASSWORD_MAX_LENGTH,
HOSTED_PASSWORD_MIN_LENGTH,
@ -130,7 +130,7 @@ function SignUpPage() {
Already have an account?{" "}
<Link
to="/sign-in"
search={getAuthLinkSearch(redirectTo)}
search={getSignInSearch(redirectTo)}
className="link link-primary"
>
Sign in

View File

@ -1,10 +1,34 @@
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { Outlet, createFileRoute, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { authRedirectSearchSchema } from "@/client/features/auth/AuthPage";
import { useSession } from "@/lib/auth-client";
import { isHostedClientAuthMode } from "@/lib/auth-mode";
import { normalizeAuthRedirect } from "@/lib/auth-redirect";
export const Route = createFileRoute("/_auth")({
component: AuthLayout,
validateSearch: authRedirectSearchSchema,
component: AuthPageLayout,
});
function AuthLayout() {
function AuthPageLayout() {
const search = Route.useSearch();
const navigate = useNavigate();
const { data: session, isPending } = useSession();
const isHostedMode = isHostedClientAuthMode();
const redirectTo = normalizeAuthRedirect(search.redirect);
useEffect(() => {
if (!session?.user?.id) {
return;
}
void navigate({ href: redirectTo, replace: true });
}, [navigate, redirectTo, session?.user?.id]);
if (isHostedMode && (isPending || session?.user?.id)) {
return null;
}
return (
<div className="min-h-[100dvh] bg-base-200">
<div className="min-h-[100dvh] flex items-center justify-center p-4">

View File

@ -6,7 +6,7 @@ const SUPPORT_EMAIL = "ben@everyapp.com";
const DATAFORSEO_MCP_DOCS_URL =
"https://dataforseo.com/help-center/setting-up-the-official-dataforseo-mcp-server-simple-guide";
export const Route = createFileRoute("/p/$projectId/ai")({
export const Route = createFileRoute("/_project/p/$projectId/ai")({
component: AiPage,
});

View File

@ -1,6 +1,6 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
export const Route = createFileRoute("/p/$projectId/audit")({
export const Route = createFileRoute("/_project/p/$projectId/audit")({
component: SiteAuditLayout,
});

View File

@ -19,8 +19,8 @@ import {
SUPPORT_URL,
} from "@/client/features/audit/shared";
export const Route = createFileRoute<"/p/$projectId/audit/">(
"/p/$projectId/audit/",
export const Route = createFileRoute<"/_project/p/$projectId/audit/">(
"/_project/p/$projectId/audit/",
)({
validateSearch: auditSearchSchema,
component: SiteAuditPage,

View File

@ -2,7 +2,9 @@ import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { PsiIssuesScreen } from "@/client/features/psi/issues/PsiIssuesScreen";
import { psiIssuesSearchSchema } from "@/types/schemas/psi";
export const Route = createFileRoute("/p/$projectId/audit/issues/$resultId")({
export const Route = createFileRoute(
"/_project/p/$projectId/audit/issues/$resultId",
)({
validateSearch: psiIssuesSearchSchema,
component: AuditIssuesPage,
});

View File

@ -3,7 +3,7 @@ import { BacklinksPage } from "@/client/features/backlinks/BacklinksPage";
import { inferBacklinksSearchScopeFromTarget } from "@/client/features/backlinks/backlinksSearchScope";
import { backlinksSearchSchema } from "@/types/schemas/backlinks";
export const Route = createFileRoute("/p/$projectId/backlinks")({
export const Route = createFileRoute("/_project/p/$projectId/backlinks")({
validateSearch: backlinksSearchSchema,
component: BacklinksRoute,
});

View File

@ -7,7 +7,7 @@ import {
} from "@/client/features/domain/utils";
import { domainSearchSchema } from "@/types/schemas/domain";
export const Route = createFileRoute("/p/$projectId/domain")({
export const Route = createFileRoute("/_project/p/$projectId/domain")({
validateSearch: domainSearchSchema,
component: DomainOverviewRoute,
});

View File

@ -1,6 +1,6 @@
import { createFileRoute, redirect } from "@tanstack/react-router";
export const Route = createFileRoute("/p/$projectId/")({
export const Route = createFileRoute("/_project/p/$projectId/")({
beforeLoad: ({ params }) => {
throw redirect({
to: "/p/$projectId/keywords",

View File

@ -10,7 +10,7 @@ import {
} from "@/client/features/keywords/keywordSearchParams";
import { keywordsSearchSchema } from "@/types/schemas/keywords";
export const Route = createFileRoute("/p/$projectId/keywords")({
export const Route = createFileRoute("/_project/p/$projectId/keywords")({
validateSearch: keywordsSearchSchema,
beforeLoad: ({ params, search }) => {
const { normalized, changed } = normalizeLegacyKeywordSearch(search);

View File

@ -0,0 +1,48 @@
import { Outlet, createFileRoute, redirect } from "@tanstack/react-router";
import { getErrorCode } from "@/client/lib/error-messages";
import { AuthenticatedAppLayout } from "@/client/layout/AppShell";
import {
getCurrentAuthRedirectFromHref,
getSignInSearch,
} from "@/lib/auth-redirect";
import { getProjectAccess } from "@/serverFunctions/projects";
export const Route = createFileRoute("/_project/p/$projectId")({
beforeLoad: async ({ location, params }) => {
try {
await getProjectAccess({ data: { projectId: params.projectId } });
} catch (error) {
if (getErrorCode(error) === "UNAUTHENTICATED") {
throw redirect({
to: "/sign-in",
search: getSignInSearch(
getCurrentAuthRedirectFromHref(location.href),
),
replace: true,
});
}
throw redirect({ to: "/", replace: true });
}
},
pendingComponent: ProjectRoutePending,
component: ProjectLayout,
});
function ProjectLayout() {
const { projectId } = Route.useParams();
return (
<AuthenticatedAppLayout projectId={projectId}>
<Outlet />
</AuthenticatedAppLayout>
);
}
function ProjectRoutePending() {
return (
<div className="flex h-full items-center justify-center">
<span className="loading loading-spinner loading-md" />
</div>
);
}

View File

@ -10,7 +10,7 @@ import { Trash2, Download, Search, Loader2, AlertCircle } from "lucide-react";
import { buildCsv, downloadCsv } from "@/client/lib/csv";
import { getStandardErrorMessage } from "@/client/lib/error-messages";
export const Route = createFileRoute("/p/$projectId/saved")({
export const Route = createFileRoute("/_project/p/$projectId/saved")({
component: SavedKeywordsPage,
});

View File

@ -0,0 +1,9 @@
import { Outlet, createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_project")({
component: ProjectRouteLayout,
});
function ProjectRouteLayout() {
return <Outlet />;
}

View File

@ -1,41 +0,0 @@
import { createFileRoute, Outlet, useNavigate } from "@tanstack/react-router";
import { useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { getProjectAccess } from "@/serverFunctions/projects";
export const Route = createFileRoute("/p/$projectId")({
component: ProjectLayout,
});
function ProjectLayout() {
const params = Route.useParams();
const { projectId } = params;
const navigate = useNavigate();
const {
data: project,
isLoading,
isError,
} = useQuery({
queryKey: ["project", projectId],
queryFn: () => getProjectAccess({ data: { projectId } }),
});
useEffect(() => {
if (!isLoading && (isError || !project)) {
void navigate({ to: "/" });
}
}, [isLoading, isError, project, navigate]);
if (isLoading) {
return (
<div className="flex items-center justify-center h-full">
<span className="loading loading-spinner loading-md" />
</div>
);
}
if (!project) return null;
return <Outlet />;
}